Files
Michael Kriese 4dc30ea9bc fix(http): share concurrent identical cache provider requests (#46627)
* fix(http): share concurrent identical cache provider requests

Identical requests that use a cache provider and run at the same time now
share one in-flight request instead of each fetching from the server.
Callers that join an in-flight request get a deep copy of the response.

This replaces the per-URL mutex, which only covered the cache check and
was released before the fetch finished.

Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io>

* fix(http): reuse cache providers for npm and typst lookups

The npm and typst datasources now use one module-level cache provider, so
concurrent lookups of the same package share one request. The request key
is only hashed for GET and HEAD requests and includes `throwHttpErrors`.

Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io>
Co-Authored-By: Claude Sonnet 5.5 <michael.kriese+claude-code@mend.io>

* fix(http): don't share requests that differ in response type

Add `responseType` and `responseBecomesConfig` to the key of shared
requests, and test that requests with different headers aren't shared
and that a shared response is copied for every caller.

Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io>
Co-Authored-By: Claude Sonnet 5.5 <michael.kriese+claude-code@mend.io>

* fix(http): keep in-flight requests per Http class and copy binary bodies

Co-Authored-By: Claude Opus 5.5 <michael.kriese+claude-code@mend.io>

---------

Co-authored-by: Claude Opus 5.5 <michael.kriese+claude-code@mend.io>
2026-10-13 15:38:10 +00:00

40 lines
1.2 KiB
TypeScript

import { HOST_BLOCKED } from '../../constants/error-messages.ts';
import { clone } from '../clone.ts';
import type { HttpResponse } from './types.ts';
// Copying will help to avoid circular structure
// and mutation of the cached response.
export function copyResponse<T>(
{ statusCode, headers, body, cached }: HttpResponse<T>,
deep: boolean,
): HttpResponse<T> {
const res: HttpResponse<T> = { statusCode, headers, body };
if (deep) {
res.headers = clone(headers);
if (Buffer.isBuffer(body)) {
// Buffer#slice returns a view, so copy it explicitly
res.body = Buffer.from(body) as T;
} else if (body instanceof Uint8Array) {
res.body = body.slice() as T;
} else {
res.body = clone<T>(body);
}
}
if (cached) {
res.cached = true;
}
return res;
}
/**
* The log message for a request the HTTP layer refused, distinguishing a host blocked by the internal-host policy from one an administrator disabled.
*
* Both are reported the same way by their callers - traced and swallowed - so only the wording tells them apart in the logs.
*/
export function refusedHostMessage(err: Error): string {
return err.message === HOST_BLOCKED ? 'Host blocked' : 'Host disabled';
}