-
Notifications
You must be signed in to change notification settings - Fork 0
fix(deploy): bound integration status requests #314
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,6 +52,8 @@ export interface ListIntegrationsOptions { | |
| resolveWorkspaceToken?: typeof resolveWorkspaceToken; | ||
| provider?: string; | ||
| includeTriggers?: boolean; | ||
| /** Maximum time for any single Cloud catalog/status request. */ | ||
| requestTimeoutMs?: number; | ||
| } | ||
|
|
||
| export class IntegrationsListError extends Error { | ||
|
|
@@ -386,26 +388,54 @@ async function requestJson( | |
| pathname: string, | ||
| init: RequestInit = {} | ||
| ): Promise<unknown> { | ||
| const response = options.client | ||
| ? await options.client.fetch(pathname, init) | ||
| : await (options.fetch ?? fetch)(`${cloudUrl}${pathname}`, { | ||
| ...init, | ||
| headers: { | ||
| accept: 'application/json', | ||
| 'content-type': 'application/json', | ||
| ...(auth.token ? { authorization: `Bearer ${auth.token}` } : {}), | ||
| ...(init.headers ?? {}) | ||
| } | ||
| }); | ||
| if (!response.ok) { | ||
| const body = await response.text().catch(() => ''); | ||
| const excerpt = body.length > 400 ? `${body.slice(0, 400)}...` : body; | ||
| throw new IntegrationsListError( | ||
| `integration catalog/status request failed: ${response.status} ${pathname}${excerpt ? ` ${excerpt}` : ''}`, | ||
| { status: response.status, endpoint: pathname, body: excerpt } | ||
| ); | ||
| const timeoutMs = options.requestTimeoutMs ?? 10_000; | ||
| const controller = new AbortController(); | ||
| const upstreamSignal = init.signal; | ||
| const abortFromUpstream = () => controller.abort(upstreamSignal?.reason); | ||
| if (upstreamSignal?.aborted) abortFromUpstream(); | ||
| else upstreamSignal?.addEventListener('abort', abortFromUpstream, { once: true }); | ||
|
|
||
| let timeout: ReturnType<typeof setTimeout> | undefined; | ||
| const timeoutError = new IntegrationsListError( | ||
| `integration catalog/status request timed out after ${timeoutMs}ms: ${pathname}`, | ||
| { status: 408, endpoint: pathname, body: '' } | ||
| ); | ||
| const timeoutPromise = new Promise<never>((_, reject) => { | ||
| timeout = setTimeout(() => { | ||
| reject(timeoutError); | ||
| controller.abort(timeoutError); | ||
| }, timeoutMs); | ||
| }); | ||
|
|
||
| try { | ||
| const request = (async (): Promise<unknown> => { | ||
| const response = options.client | ||
| ? await options.client.fetch(pathname, { ...init, signal: controller.signal }) | ||
| : await (options.fetch ?? fetch)(`${cloudUrl}${pathname}`, { | ||
| ...init, | ||
| signal: controller.signal, | ||
| headers: { | ||
| accept: 'application/json', | ||
| 'content-type': 'application/json', | ||
| ...(auth.token ? { authorization: `Bearer ${auth.token}` } : {}), | ||
| ...(init.headers ?? {}) | ||
| } | ||
| }); | ||
| if (!response.ok) { | ||
| const body = await response.text().catch(() => ''); | ||
| const excerpt = body.length > 400 ? `${body.slice(0, 400)}...` : body; | ||
| throw new IntegrationsListError( | ||
| `integration catalog/status request failed: ${response.status} ${pathname}${excerpt ? ` ${excerpt}` : ''}`, | ||
| { status: response.status, endpoint: pathname, body: excerpt } | ||
| ); | ||
| } | ||
| return await response.json(); | ||
| })(); | ||
| return await Promise.race([request, timeoutPromise]); | ||
| } finally { | ||
| if (timeout) clearTimeout(timeout); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an endpoint sends response headers but then stalls before completing the body, Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Audited at HEAD You are right about the code you reviewed. This comment is anchored to What const request = (async (): Promise<unknown> => {
const response = options.client ? await options.client.fetch(...) : await (options.fetch ?? fetch)(...);
if (!response.ok) {
const body = await response.text().catch(() => ''); // <- now inside the race
throw new IntegrationsListError(...);
}
return await response.json(); // <- now inside the race
})();
return await Promise.race([request, timeoutPromise]);
} finally {
if (timeout) clearTimeout(timeout);
upstreamSignal?.removeEventListener('abort', abortFromUpstream);
}Both cleanups you asked about — the timer and the upstream abort-listener removal — are in a Regression test + non-vacuity proof. I proved it is not vacuous rather than assuming it. I reverted only The 506 ms duration is the tell: the 500 ms test guard fired because No code change needed at HEAD |
||
| upstreamSignal?.removeEventListener('abort', abortFromUpstream); | ||
| } | ||
| return await response.json(); | ||
| } | ||
|
|
||
| function adapterSlugForCloudProvider(provider: string): string { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.