@waiting/fetch
Version:
HTTP fetch API for browser and Node.js. Handle 302/303 redirect correctly on Node.js
150 lines (124 loc) • 3.73 kB
text/typescript
import assert from 'node:assert'
import type { Span } from '@opentelemetry/api'
import type {
RequestInfo,
RequestInit,
Response,
} from 'undici'
import { fetch } from 'undici'
import { trace } from './trace.js'
import type { Args } from './types.js'
import { AttributeKey } from './types.js'
import {
parseRespCookie,
processInitOpts,
processRequestGetLikeData,
processRequestPostLikeData,
} from './util.js'
/**
* fetch wrapper
*
* parameter init ignored during parameter input is typeof Request
*/
export async function _fetch(
input: RequestInfo,
args: Args,
requestInit: RequestInit,
span?: Span,
): Promise<Response> {
const resp = await createRequest(input, args, requestInit, span)
const res = await handleRedirect(resp, args, requestInit)
trace(AttributeKey.HandleRedirectFinish, span)
return res
}
export async function createRequest(
input: RequestInfo,
args: Args,
requestInit: RequestInit,
span?: Span,
): Promise<Response> {
let inputNew = input
const fetchModule = fetch
let resp: Response
// @ts-expect-error
delete requestInit.otelComponent
// @ts-expect-error
delete requestInit.span
// @ts-expect-error
delete requestInit.traceContext
if (typeof input === 'string') {
trace(AttributeKey.ProcessRequestData)
assert(input, 'input should not be empty when typeof input is string')
assert(requestInit.method, 'requestInit.method should not be empty')
if (['GET', 'DELETE'].includes(requestInit.method)) {
inputNew = processRequestGetLikeData(input, args)
}
else if (['POST', 'PUT', 'OPTIONS'].includes(requestInit.method)) {
const body = processRequestPostLikeData(args) ?? null
requestInit.body = body
}
else {
throw new TypeError(`Invalid method value: "${requestInit.method}"`)
}
// fix undici not support referrer value 'client' and 'no-referrer'
const { referrer } = requestInit
if (! referrer || referrer === 'client' || referrer === 'no-referrer') {
// @ts-expect-error
requestInit.referrer = void 0
}
// @ts-expect-error
if (requestInit.url) {
// @ts-expect-error
delete requestInit.url
}
trace(AttributeKey.RequestStart, span)
resp = await fetchModule(inputNew, requestInit)
}
else {
trace(AttributeKey.RequestStart, span)
resp = await fetchModule(input)
}
trace(AttributeKey.RequestFinish, span)
return resp
}
/**
* Handle redirect case to retrieve cookies before jumping under Node.js.
* There's no effect under Browser
*
* docs: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
*/
export async function handleRedirect(
resp: Response,
args: Args,
init: RequestInit,
): Promise<Response> {
// test by test/30_cookie.test.ts
if (args.keepRedirectCookies === true && resp.status >= 301 && resp.status <= 308) {
// do NOT use resp.url, it's the final url
const location = resp.headers.get('location')
if (! location) {
return resp
}
const url = location.toLocaleLowerCase().startsWith('http')
? location
: new URL(location, resp.url).toString()
if (url) {
const cookie = resp.headers.get('Set-Cookie')
const cookieObj = parseRespCookie(cookie)
if (cookieObj) {
args.cookies = args.cookies
? { ...args.cookies, ...cookieObj }
: { ...cookieObj }
}
const options = processInitOpts({ args, requestInit: init })
if (resp.status === 303) {
options.requestInit.method = 'GET'
return _fetch(url, options.args, options.requestInit)
}
else {
return _fetch(url, options.args, options.requestInit)
}
}
}
return resp
}