mockttp
Version:
Mock HTTP server for testing HTTP clients and stubbing webservices
156 lines (134 loc) • 5.81 kB
text/typescript
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<T extends {
headers: Headers,
body: CompletedBody
}>(
input: T,
bodySerializer: BodySerializer
): Promise<Replace<T, { body: SerializedBody }>> {
return {
...input,
body: await bodySerializer(input.body, input.headers)
};
}
export type BodySerializer = (body: CompletedBody, headers: Headers) => MaybePromise<SerializedBody>;
// Client-side: turn a serialized body back into a CompletedBody (body to be exposed for convenient access)
export function withDeserializedBodyReader<T extends { headers: Headers, body: CompletedBody }>(
input: Replace<T, { body: SerializedBody }>
): 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 + <v4 servers).
: failIfDecodingRequired.bind(null, decodingError);
return buildBodyReader(encodedBody, headers, decoder);
}
function failIfDecodingRequired(errorMessage: string | undefined, buffer: Buffer, headers: Headers) {
if (!headers['content-encoding'] || headers['content-encoding'] === 'identity') {
return buffer;
}
const error = errorMessage
? new Error(`Decoding error (${headers['content-encoding']}): ${errorMessage}`)
: new Error("Can't read encoded message body without server-side decoding");
console.warn(error.message);
throw error;
}
/**
* Serialize a callback result (callback handlers, BeforeRequest/Response etc)
* to transform all the many possible buffer formats into either base64-encoded
* buffer data, or undefined.
*/
export function withSerializedCallbackBuffers<T extends {
body?: CompletedBody | Buffer | Uint8Array | ArrayBuffer | string,
rawBody?: Buffer | Uint8Array
}>(input: T): Replace<T, { body: string | undefined }> {
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<T extends { body?: any }> =
Replace<T, { body?: string, rawBody?: string }>;
/**
* 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<T extends {
body?: Buffer | Uint8Array | string,
rawBody?: Buffer | Uint8Array
}>(
input: Replace<T, { body?: string, rawBody?: string }>
): 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;
}