diff --git a/.changeset/short-years-press.md b/.changeset/short-years-press.md new file mode 100644 index 00000000000..2be7f877c65 --- /dev/null +++ b/.changeset/short-years-press.md @@ -0,0 +1,5 @@ +--- +"@atproto-labs/fetch": minor +--- + +Fix typo in `ResponseTranformer` and `fetchResponseJsonTranformer` diff --git a/.changeset/twenty-ghosts-tell.md b/.changeset/twenty-ghosts-tell.md new file mode 100644 index 00000000000..f89ca5335b8 --- /dev/null +++ b/.changeset/twenty-ghosts-tell.md @@ -0,0 +1,5 @@ +--- +"@atproto-labs/fetch": patch +--- + +Response mime type check is now case-insensitive (as per rfc2616) diff --git a/packages/internal/fetch/src/fetch-response.ts b/packages/internal/fetch/src/fetch-response.ts index 36f5a06aa0f..c4830e1ce0e 100644 --- a/packages/internal/fetch/src/fetch-response.ts +++ b/packages/internal/fetch/src/fetch-response.ts @@ -14,7 +14,24 @@ import { logCancellationError, } from './util.js' -export type ResponseTranformer = Transformer +/** + * media-type = type "/" subtype *( ";" parameter ) + * type = token + * subtype = token + * token = 1* + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + * CTL = + * SP = + * HT = + * @note The type, subtype, and parameter attribute names are case-insensitive. + * @see {@link https://datatracker.ietf.org/doc/html/rfc2616#autoid-23} + */ +const JSON_MIME = /^application\/(?:[^()<>@,;:/[\]\\?={} \t]+\+)?json$/i + +export type ResponseTransformer = Transformer export type ResponseMessageGetter = Transformer export class FetchResponseError extends FetchError { @@ -51,7 +68,7 @@ const extractResponseMessage: ResponseMessageGetter = async (response) => { try { if (mimeType === 'text/plain') { return await response.text() - } else if (/^application\/(?:[^+]+\+)?json$/i.test(mimeType)) { + } else if (JSON_MIME.test(mimeType)) { const json: unknown = await response.json() if (typeof json === 'string') return json @@ -155,7 +172,7 @@ export function cancelBodyOnError( export function fetchOkProcessor( customMessage?: string | ResponseMessageGetter, -): ResponseTranformer { +): ResponseTransformer { return cancelBodyOnError((response) => { return fetchOkTransformer(response, customMessage) }) @@ -169,7 +186,7 @@ export async function fetchOkTransformer( throw await FetchResponseError.from(response, customMessage) } -export function fetchMaxSizeProcessor(maxBytes: number): ResponseTranformer { +export function fetchMaxSizeProcessor(maxBytes: number): ResponseTransformer { if (maxBytes === Infinity) return (response) => response if (!Number.isFinite(maxBytes) || maxBytes < 0) { throw new TypeError('maxBytes must be a 0, Infinity or a positive number') @@ -200,7 +217,7 @@ export type MimeTypeCheck = string | RegExp | MimeTypeCheckFn export function fetchTypeProcessor( expectedMime: MimeTypeCheck, contentTypeRequired = true, -): ResponseTranformer { +): ResponseTransformer { const isExpected: MimeTypeCheckFn = typeof expectedMime === 'string' ? (mimeType) => mimeType === expectedMime @@ -220,7 +237,7 @@ export async function fetchResponseTypeChecker( ): Promise { const mimeType = extractMime(response) if (mimeType) { - if (!isExpectedMime(mimeType)) { + if (!isExpectedMime(mimeType.toLowerCase())) { throw await FetchResponseError.from( response, `Unexpected response Content-Type (${mimeType})`, @@ -243,7 +260,7 @@ export type ParsedJsonResponse = { json: T } -export async function fetchResponseJsonTranformer( +export async function fetchResponseJsonTransformer( response: Response, ): Promise> { try { @@ -260,12 +277,12 @@ export async function fetchResponseJsonTranformer( } export function fetchJsonProcessor( - expectedMime: MimeTypeCheck = /^application\/(?:[^+]+\+)?json$/, + expectedMime: MimeTypeCheck = JSON_MIME, contentTypeRequired = true, ): Transformer> { return pipe( fetchTypeProcessor(expectedMime, contentTypeRequired), - cancelBodyOnError(fetchResponseJsonTranformer), + cancelBodyOnError(fetchResponseJsonTransformer), ) }