import { Buffer } from 'buffer'; import * as _ from 'lodash'; import { encode as encodeBase64 } from 'base64-arraybuffer'; import { MaybePromise, UnreachableCheck } from '@httptoolkit/util'; import { CompletedBody, Headers } from "../types"; import { asBuffer } from "../util/buffer-utils"; import { buildBodyReader, isMockttpBody } from "../util/request-utils"; import { Replace } from "../util/type-utils"; import { deserializeBuffer, serializeBuffer } from "./serialization"; export type SerializedBody = // Base64 string of encoded body, from 'none' body decoding option, or old servers: | string // Was encoded, now decoded successfully: | { encoded: string, decoded: string, decodingError: undefined } // Trivially known that no decoding was used: | { encoded: string, decoded: undefined, decodingError: undefined } // Was encoded, but decoding failed: | { encoded: string, decodingError: string, decoded: undefined }; // Server-side: serialize a body, so it can become a CompletedBody on the client side export async function withSerializedBodyReader( input: T, bodySerializer: BodySerializer ): Promise> { return { ...input, body: await bodySerializer(input.body, input.headers) }; } export type BodySerializer = (body: CompletedBody, headers: Headers) => MaybePromise; // Client-side: turn a serialized body back into a CompletedBody (body to be exposed for convenient access) export function withDeserializedBodyReader( input: Replace ): T { let encodedBodyString: string; let decodedBodyString: string | undefined; let decodedBodyError: string | undefined; // We don't need to know the expected serialization format: we can detect it, and just // use what we get sensibly regardless: if (typeof input.body === 'string') { // If the body is a string, it is a base64-encoded string encodedBodyString = input.body; } else if (typeof input.body === 'object') { encodedBodyString = input.body.encoded; decodedBodyString = input.body.decoded; decodedBodyError = input.body.decodingError; } else { throw new UnreachableCheck(input.body); } return { ...input, body: deserializeBodyReader(encodedBodyString, decodedBodyString, decodedBodyError, input.headers), } as T; } export function deserializeBodyReader( encodedBodyString: string, decodedBodyString: string | undefined, decodingError: string | undefined, headers: Headers ): CompletedBody { const encodedBody = deserializeBuffer(encodedBodyString); const decodedBody = decodedBodyString ? deserializeBuffer(decodedBodyString) : undefined; const decoder = !!decodedBody // If the server provides a pre-decoded body, we use it. ? async () => decodedBody // If not, all encoded bodies are non-decodeable on the client side. This should // only happen with messageBodyDecoding = 'none' (or with v4+ clients + (input: T): Replace { let serializedBody: string | undefined; if (!input.body) { serializedBody = undefined; } else if (_.isString(input.body)) { serializedBody = serializeBuffer(asBuffer(input.body)); } else if (_.isBuffer(input.body)) { serializedBody = serializeBuffer(input.body as Buffer); } else if (_.isArrayBuffer(input.body) || _.isTypedArray(input.body)) { serializedBody = encodeBase64(input.body as ArrayBuffer); } else if (isMockttpBody(input.body)) { serializedBody = serializeBuffer(asBuffer(input.body.buffer)); } return { ...input, body: serializedBody, rawBody: input.rawBody ? serializeBuffer(asBuffer(input.rawBody)) : undefined }; } export type WithSerializedCallbackBuffers = Replace; /** * Deserialize a callback result (callback handlers, BeforeRequest/Response etc) * to build buffer data (or undefined) from the base64-serialized data * produced by withSerializedCallbackBuffers */ export function withDeserializedCallbackBuffers( input: Replace ): T { return { ...input, body: input.body !== undefined ? Buffer.from(input.body, 'base64') : undefined, rawBody: input.rawBody !== undefined ? Buffer.from(input.rawBody, 'base64') : undefined } as T; }