fix: give every upstream mail server request a deadline

This commit is contained in:
Linus Rath
2026-10-19 01:32:28 +02:00
parent 1f11cc7249
commit f35ed6fb02
2 changed files with 22 additions and 1 deletions
+15
View File
@@ -70,6 +70,21 @@ describe('fetchJmapServer', () => {
expect(guardedFetch).not.toHaveBeenCalled();
});
it('gives every request a deadline unless the caller brought one', async () => {
// A slow or silent server (with custom endpoints on, one the user chose)
// used to be able to hold the connection open for as long as it liked.
await fetchJmapServer('http://10.0.0.5/jmap/', { method: 'POST', body: '{}' }, true);
expect(plainFetch.mock.calls[0][1].signal).toBeInstanceOf(AbortSignal);
guardedFetch.mockResolvedValueOnce(response(200, {}, 'guarded'));
await fetchJmapServer('https://custom.example/jmap/', { method: 'GET' }, false);
expect(guardedFetch.mock.calls[0][1].signal).toBeInstanceOf(AbortSignal);
const own = new AbortController().signal;
await fetchJmapServer('http://10.0.0.5/jmap/', { method: 'GET', signal: own }, true);
expect(plainFetch.mock.calls[1][1].signal).toBe(own);
});
it('routes untrusted servers through fetchPublicUrl', async () => {
guardedFetch.mockResolvedValueOnce(response(200, {}, 'guarded'));
const res = await fetchJmapServer(
+7 -1
View File
@@ -20,6 +20,8 @@ import { parseJmapServers, resolveTrustedJmapUrl } from '@/lib/admin/jmap-server
import { fetchPublicUrl, type PublicFetchResponse } from '@/lib/security/url-guard';
const MAX_REDIRECTS = 5;
/** Deadline for one upstream request when the caller sets none. */
export const UPSTREAM_TIMEOUT_MS = 30_000;
export async function isTrustedJmapServerUrl(serverUrl: string): Promise<boolean> {
await configManager.ensureLoaded();
@@ -56,9 +58,13 @@ function headersToRecord(init: HeadersInit | undefined): Record<string, string>
*/
export async function fetchJmapServer(
url: string,
init: ServerFetchInit = {},
givenInit: ServerFetchInit = {},
trusted: boolean | undefined,
): Promise<Response> {
// Every call gets a deadline: a slow or silent server (with custom
// endpoints on, one the user chose) must not pin a connection for as long
// as it likes. Callers with their own signal keep it.
const init: ServerFetchInit = givenInit.signal ? givenInit : { ...givenInit, signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS) };
if (trusted !== false) {
return fetch(url, init);
}