← Public packages
@kentcdodds/cloudflare
Cloudflare helpers: API v4, static site publishing to Pages, analytics, WAF rules, logs, and docs.
src/request-body.ts
111 lines · 4.2 KB · TypeScript/**
* Request body builders for Cloudflare API v4 calls: JSON, multipart form, or
* raw bytes/text. Pure (no network).
*/
import { base64ToBytes } from './static-site.ts'
/**
* One multipart field: a plain string, or a file part. File parts take UTF-8
* `content` or `base64` bytes, plus optional `contentType`.
*/
export type FormFieldValue =
| string
| {
filename: string
content?: string
base64?: string
contentType?: string
}
export type FormFields = Record<string, FormFieldValue>
/** Raw body: text sent as-is, bytes, or `{ base64 }` for JSON callers. */
export type RawBody = string | Uint8Array | ArrayBuffer | { base64: string }
export type BuiltBody = {
body: BodyInit | undefined
/** Content-Type to set, or `null` to let fetch set it (multipart boundary). */
contentType: string | null
kind: 'none' | 'json' | 'form' | 'raw'
}
/** Build a FormData from string / file fields. */
export function buildFormData(fields: FormFields): FormData {
if (!fields || typeof fields !== 'object' || Array.isArray(fields)) {
throw new Error('form must be an object of field names to string or { filename, content | base64 }.')
}
const form = new FormData()
for (const [name, value] of Object.entries(fields)) {
if (typeof value === 'string') {
form.append(name, value)
continue
}
if (!value || typeof value !== 'object' || typeof value.filename !== 'string' || !value.filename) {
throw new Error(`form["${name}"] must be a string or { filename, content | base64, contentType? }.`)
}
const unknown = Object.keys(value).filter(
(key) => !['filename', 'content', 'base64', 'contentType'].includes(key),
)
if (unknown.length) {
throw new Error(`form["${name}"] has unknown key(s): ${unknown.join(', ')}.`)
}
const hasContent = typeof value.content === 'string'
const hasBase64 = typeof value.base64 === 'string'
if (hasContent === hasBase64) {
throw new Error(`form["${name}"] needs exactly one of content or base64.`)
}
const bytes = hasBase64 ? base64ToBytes(value.base64 as string) : new TextEncoder().encode(value.content)
const type = value.contentType ?? (hasBase64 ? 'application/octet-stream' : 'text/plain')
form.append(name, new File([bytes as BlobPart], value.filename, { type }))
}
return form
}
/** Normalize a raw body into something fetch accepts. */
export function buildRawBody(raw: RawBody): BodyInit {
if (typeof raw === 'string') return raw
if (raw instanceof Uint8Array) return raw as BodyInit
if (raw instanceof ArrayBuffer) return new Uint8Array(raw) as BodyInit
if (raw && typeof raw === 'object' && typeof (raw as { base64?: unknown }).base64 === 'string') {
return base64ToBytes((raw as { base64: string }).base64) as BodyInit
}
throw new Error('rawBody must be a string, Uint8Array, ArrayBuffer, or { base64 }.')
}
/**
* Pick exactly one of `body` (JSON), `form` (multipart), or `rawBody`.
* GET requests never carry a body (a JSON `body` on GET is ignored, as before;
* `form` / `rawBody` on GET throw).
*/
export function buildRequestBody(
params: { body?: unknown; form?: FormFields; rawBody?: RawBody; contentType?: string },
method: string,
): BuiltBody {
const provided = (['body', 'form', 'rawBody'] as const).filter(
(key) => params[key] !== undefined,
)
if (provided.length > 1) {
throw new Error(`Pass only one of body, form, or rawBody (got ${provided.join(', ')}).`)
}
if (params.contentType !== undefined && provided[0] !== 'rawBody') {
throw new Error('contentType only applies with rawBody (JSON and form set their own).')
}
if (provided.length === 0) return { body: undefined, contentType: null, kind: 'none' }
if (method === 'GET') {
// Historical api-v4 behavior: a JSON body on GET is ignored.
if (provided[0] === 'body') return { body: undefined, contentType: null, kind: 'none' }
throw new Error(`GET requests cannot send ${provided[0]}.`)
}
if (provided[0] === 'form') {
return { body: buildFormData(params.form as FormFields), contentType: null, kind: 'form' }
}
if (provided[0] === 'rawBody') {
return {
body: buildRawBody(params.rawBody as RawBody),
contentType: params.contentType ?? 'application/octet-stream',
kind: 'raw',
}
}
return { body: JSON.stringify(params.body), contentType: 'application/json', kind: 'json' }
}