From d803d46abd6e4a086c52295845fdf288d201d939 Mon Sep 17 00:00:00 2001 From: SofianD Date: Thu, 8 Feb 2024 16:40:38 +0100 Subject: [PATCH 1/9] feat: add tool to decompress and parse. feat: handle errors by stage refactor: use class to handle http error chore: export 'UndiciResponseHandler', isHTTPError and isHttpieError docs: add doc about error handling --- README.md | 7 +- docs/errors.md | 137 +++++++++++++ docs/request.md | 34 ++-- examples/mode.ts | 17 ++ examples/throwOnHttpieError.mjs | 19 ++ src/class/HttpieCommonError.ts | 23 +++ src/class/HttpieHandlerError.ts | 71 +++++++ src/class/HttpieOnHttpError.ts | 21 ++ src/class/undiciResponseHandler.ts | 146 ++++++++++++++ src/common/errors.ts | 53 +++++ src/index.ts | 4 +- src/request.ts | 36 ++-- src/stream.ts | 2 +- src/utils.ts | 65 ++----- test/HttpieOnHttpError.spec.ts | 41 ++++ test/request.spec.ts | 14 +- test/server/index.ts | 6 +- test/stream.spec.ts | 10 +- test/undiciResponseHandler.spec.ts | 298 +++++++++++++++++++++++++++++ test/utils.spec.ts | 146 +++----------- 20 files changed, 926 insertions(+), 224 deletions(-) create mode 100644 docs/errors.md create mode 100644 examples/mode.ts create mode 100644 examples/throwOnHttpieError.mjs create mode 100644 src/class/HttpieCommonError.ts create mode 100644 src/class/HttpieHandlerError.ts create mode 100644 src/class/HttpieOnHttpError.ts create mode 100644 src/class/undiciResponseHandler.ts create mode 100644 src/common/errors.ts create mode 100644 test/HttpieOnHttpError.spec.ts create mode 100644 test/undiciResponseHandler.spec.ts diff --git a/README.md b/README.md index 3d08f2e..e5259e5 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ The package is inspired by lukeed [httpie](https://github.com/lukeed/httpie) (Th ## 🔬 Features -- Automatically handles JSON requests and responses. +- Automatically parse based on the `content-type`. +- Automatically decompress based on the `content-encoding`. - Includes aliases for common HTTP verbs: `get`, `post`, `put`, `patch`, and `del`. - Able to automatically detect domains and paths to assign the right Agent (use a LRU cache to avoid repetitive computation). - Allows to use an accurate rate-limiter like `p-ratelimit` with the `limit` option. @@ -118,6 +119,10 @@ const response = (await httpie.safePost("https://jsonplaceholder.typicode.com/po - [Retry API](./docs/retry.md) - [Work and manage Agents](./docs/agents.md) +## Error handling + +Read the [error documentation](./docs/errors.md). + ## Contributors ✨ diff --git a/docs/errors.md b/docs/errors.md new file mode 100644 index 0000000..dcc9d09 --- /dev/null +++ b/docs/errors.md @@ -0,0 +1,137 @@ +# ERRORS + +All errors generated by Httpie failure inherit [`HttpieError`](../src/class/HttpieCommonError.ts). + +```ts +interface HttpieError { + headers: IncomingHttpHeaders; + statusCode: number; +} +``` + +## Tools + +### isHttpieError + +The `isHttpieError` function can be used to find out if the error is an Httpie error. If the result is `false`, it is probably an error by Undici. +```ts +function isHttpieError(error: any): boolean; +``` + +Example: +```ts +import * as httpie from "@myunisoft/httpie"; + +try { + await httpie.request("GET", "127.0.0.1"); +} +catch (error) { + if (httpie.isHttpieError(error)) { + // This error inherits from HttpieError. + console.log(Boolean(error.headers)) // true + console.log(Boolean(error.statusCode)) // true + } + else { + // This error can be of any type of error. + console.log(Boolean(error.headers)) // false + console.log(Boolean(error.statusCode)) // false + } +} +``` + +### isHTTPError + +The `isHTTPError` function can be used to find out if it is an HTTP error. +```ts +function isHTTPError(error: any): boolean; +``` + +Example: +```ts +import * as httpie from "@myunisoft/httpie"; + +try { + await httpie.request("GET", "127.0.0.1"); +} +catch (error) { + if (httpie.isHTTPError(error)) { + console.log(Boolean(error.data)) // true + console.log(Boolean(error.statusMessage)) // true + } + else { + console.log(Boolean(error.data)) // false + console.log(Boolean(error.statusMessage)) // false + } +} +``` + +--- + +## HTTP errors + +If the `RequestOptions.throwOnHttpError` option is set to true, all HTTP responses with a status code higher than 400 will generate an `HttpieOnHttpError` error. + +> [NOTE] Use [`isHTTPError`](#ishttperror) function to know if it is an HTTP error. + +```ts +interface HttpieOnHttpError { + statusCode: number; + statusMessage: string; + headers: IncomingHttpHeaders; + data: T; +} +``` + +## Failed to retrieve response body + +```ts +interface HttpieFetchBodyError { + statusCode: number; + headers: IncomingHttpHeaders; + message: string; + /** @description original error */ + error?: Error; +} +``` + +## Failed to decompress response body + +If the `RequestOptions.mode` option is set with `decompress` or `parse`, Httpie will try to decompress the response body based on the **content-encoding** header. + +If Httpie fails to decompress the response body, an `HttpieDecompressionError` will be raised. + +```ts +interface HttpieDecompressionError { + statusCode: number; + headers: IncomingHttpHeaders; + message: string; + /** @description original error */ + error?: Error; + /** @description original body as buffer */ + buffer: Buffer; + /** @description encodings from 'content-encoding' header */ + encodings: string[]; +} +``` + +## Failed to parse response body + +If the `RequestOptions.mode` option is set with `parse`, Httpie will try to parse the response body based on the **content-type** header. + +If Httpie fails to parse the response body, an `HttpieParserError` will be raised. + +```ts +interface HttpieParserError extends IHttpieHandlerError { + statusCode: number; + headers: IncomingHttpHeaders; + message: string; + /** @description original error */ + error?: Error; + /** @description content-type from 'content-type' header without params */ + contentType: string; + /** @description original body as buffer */ + buffer: Buffer; + /** @description body as string */ + text: string | null; +} +``` diff --git a/docs/request.md b/docs/request.md index 96f1390..9eb30cd 100644 --- a/docs/request.md +++ b/docs/request.md @@ -4,17 +4,24 @@ The request method is the root method for making http requests. Short method lik The method **options** and **response** are described by the following TypeScript interfaces: ```ts +type ModeOfHttpieResponseHandler = "decompress" | "parse" | "raw"; + export interface RequestOptions { - /** Default: 0 */ + /** @default 0 */ maxRedirections?: number; - /** Default: { "user-agent": "httpie" } */ + /** @default{ "user-agent": "httpie" } */ headers?: IncomingHttpHeaders; + querystring?: string | URLSearchParams; body?: any; authorization?: string; // Could be dynamically computed depending on the provided URI. - agent?: undici.Agent; - // API limiter from a package like "p-ratelimit" + agent?: undici.Agent | undici.ProxyAgent | undici.MockAgent; + /** @description API limiter from a package like `p-ratelimit`. */ limit?: InlineCallbackAction; + /** @default "parse" */ + mode?: ModeOfHttpieResponseHandler; + /** @default true */ + throwOnHttpError?: boolean; } export interface RequestResponse { @@ -55,22 +62,3 @@ export const put = request.bind(null, "PUT") as RequestCallback; export const del = request.bind(null, "DELETE") as RequestCallback; export const patch = request.bind(null, "PATCH") as RequestCallback; ``` - -## error - -Errors are triggered if the returned statusCode is equal or higher than 400. It can occur in case of error when reading the response body (for example an invalid JSON). - -The triggered error is constructed as follows: - -```ts -export function toError(response: RequestResponse) { - const err = new Error(response.statusMessage) as Error & RequestResponse; - err.statusMessage = response.statusMessage; - err.statusCode = response.statusCode; - err.headers = response.headers; - err.data = response.data; - - return err; -} -``` - diff --git a/examples/mode.ts b/examples/mode.ts new file mode 100644 index 0000000..1c3dd2a --- /dev/null +++ b/examples/mode.ts @@ -0,0 +1,17 @@ +import * as httpie from "../dist/index.js"; +// import * as httpie from "@myunisoft/httpie"; + +async() => { + const { data } = await httpie.request("GET", "127.0.0.1", { mode: "raw" }); + console.log(data) // Buffer +} + +async() => { + const { data } = await httpie.request("GET", "127.0.0.1", { mode: "decompress" }); + console.log(data) // Buffer +} + +async() => { + const { data } = await httpie.request<{ key: "value" }>("GET", "127.0.0.1", { mode: "raw" }); + console.log(data) // [Object] { key: "value" } +} diff --git a/examples/throwOnHttpieError.mjs b/examples/throwOnHttpieError.mjs new file mode 100644 index 0000000..12e79fd --- /dev/null +++ b/examples/throwOnHttpieError.mjs @@ -0,0 +1,19 @@ +import * as httpie from "../dist/index.js"; +// import * as httpie from "@myunisoft/httpie"; + +// Should not throw +async() => { + const { statusCode } = await httpie.request("GET", "127.0.0.1", { throwOnHttpError: false }); + console.log(statusCode) // 500 +} + +// Should throw +async() => { + let statusCode; + try { + (statusCode = await httpie.request("GET", "127.0.0.1", { throwOnHttpError: true })); + } catch (error) { + console.log(statusCode) // undefined + console.log(error.statusCode) // 500 + } +} diff --git a/src/class/HttpieCommonError.ts b/src/class/HttpieCommonError.ts new file mode 100644 index 0000000..9895eb9 --- /dev/null +++ b/src/class/HttpieCommonError.ts @@ -0,0 +1,23 @@ +// Import Third-party Dependencies +import { IncomingHttpHeaders } from "undici/types/header"; + +type CommonResponseData = { + statusCode: number; + headers: IncomingHttpHeaders; +} + +export interface IHttpieErrorOptions { + response: CommonResponseData; +} + +export class HttpieError extends Error { + headers: IncomingHttpHeaders; + statusCode: number; + + constructor(message: string, options: IHttpieErrorOptions) { + super(message); + + this.statusCode = options.response.statusCode; + this.headers = options.response.headers; + } +} diff --git a/src/class/HttpieHandlerError.ts b/src/class/HttpieHandlerError.ts new file mode 100644 index 0000000..a755ab6 --- /dev/null +++ b/src/class/HttpieHandlerError.ts @@ -0,0 +1,71 @@ +/* eslint-disable max-classes-per-file */ + +// Import Third-party Dependencies +import { HttpieError, IHttpieErrorOptions } from "./HttpieCommonError"; +import { getDecompressionError, getFetchError, getParserError } from "../common/errors"; + +interface IHttpieHandlerError extends IHttpieErrorOptions { + /** @description original error */ + error?: Error; + message: T; +} + +interface IHttpieDecompressionErrorOptions extends IHttpieHandlerError[0]["message"]> { + /** @description original body as buffer */ + buffer: Buffer; + /** @description encodings from 'content-encoding' header */ + encodings: string[]; +} + +// eslint-disable-next-line max-len +interface IHttpieParserErrorOptions extends IHttpieHandlerError[0]["message"]> { + /** @description content-type from 'content-type' header without params */ + contentType: string; + /** @description original body as buffer */ + buffer: Buffer; + /** @description body as string */ + text: string | null; +} + +class HttpieHandlerError extends HttpieError { + reason: Error | null; + + constructor(message: string, options: IHttpieHandlerError) { + super(message, options); + + this.name = options.message; + this.reason = options.error ?? null; + } +} + +export class HttpieFetchBodyError extends HttpieHandlerError { + constructor(options: IHttpieHandlerError[0]["message"]>, ...args) { + super(getFetchError(options, ...args), options); + } +} + +export class HttpieDecompressionError extends HttpieHandlerError { + buffer: Buffer; + encodings: string[]; + + constructor(options: IHttpieDecompressionErrorOptions, ...args) { + super(getDecompressionError(options, ...args), options); + + this.buffer = options.buffer; + this.encodings = options.encodings; + } +} + +export class HttpieParserError extends HttpieHandlerError { + contentType: string; + buffer: Buffer; + text: string | null; + + constructor(options: IHttpieParserErrorOptions, ...args) { + super(getParserError(options, ...args), options); + + this.buffer = options.buffer; + this.contentType = options.contentType; + this.text = options.text ?? null; + } +} diff --git a/src/class/HttpieOnHttpError.ts b/src/class/HttpieOnHttpError.ts new file mode 100644 index 0000000..3ebc98a --- /dev/null +++ b/src/class/HttpieOnHttpError.ts @@ -0,0 +1,21 @@ +// Import Internal Dependencies +import { HttpieError } from "./HttpieCommonError"; +import { RequestResponse } from "../request"; + +/** + * @description Class to generate an Error with all the required properties from the response. + * We attach them to the error so that they can be retrieved by the developer in a Catch block. + */ +export class HttpieOnHttpError> extends HttpieError { + name = "HttpieOnHttpError"; + + statusMessage: string; + data: T; + + constructor(response: T) { + super(response.statusMessage, { response }); + + this.statusMessage = response.statusMessage; + this.data = response.data; + } +} diff --git a/src/class/undiciResponseHandler.ts b/src/class/undiciResponseHandler.ts new file mode 100644 index 0000000..da5b7e8 --- /dev/null +++ b/src/class/undiciResponseHandler.ts @@ -0,0 +1,146 @@ +/* eslint-disable no-dupe-class-members */ + +// Import Node.js Dependencies +import { promisify } from "node:util"; +import { inflate, brotliDecompress, gunzip } from "node:zlib"; + +// Import Third-party Dependencies +import { Dispatcher } from "undici"; +import * as contentType from "content-type"; + +// Import Internal Dependencies +import { getEncodingCharset } from "../utils"; +import { HttpieDecompressionError, HttpieFetchBodyError, HttpieParserError } from "./HttpieHandlerError"; + +const kAsyncGunzip = promisify(gunzip); +const kDecompress = { + gzip: kAsyncGunzip, + "x-gzip": kAsyncGunzip, + br: promisify(brotliDecompress), + deflate: promisify(inflate) +}; + +export type TypeOfDecompression = keyof typeof kDecompress; +export type ModeOfHttpieResponseHandler = "decompress" | "parse" | "raw"; + +export class HttpieResponseHandler { + response: Dispatcher.ResponseData; + + constructor(response: Dispatcher.ResponseData) { + this.response = response; + } + + getData(mode: "decompress" | "raw"): Promise; + getData(mode?: "parse"): Promise; + getData(mode: ModeOfHttpieResponseHandler = "parse") { + if (mode === "parse") { + return this.parseUndiciResponse(); + } + + if (mode === "decompress") { + return this.getDecompressedBuffer(); + } + + return this.getBuffer(); + } + + private async getBuffer(): Promise { + try { + return Buffer.from(await this.response.body.arrayBuffer()); + } + catch (error) { + throw new HttpieFetchBodyError({ + message: "ResponseFetchError", + error, + response: this.response + }); + } + } + + private async getDecompressedBuffer(): Promise { + const buffer = await this.getBuffer(); + const encodingHeader = this.response.headers["content-encoding"]; + + if (!encodingHeader) { + return buffer; + } + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding#syntax + const encodings = Array.isArray(encodingHeader) ? + encodingHeader.reverse() : + encodingHeader.split(",").reverse(); + + let decompressedBuffer = Buffer.from(buffer); + for (const rawEncoding of encodings) { + const encoding = rawEncoding.trim() as TypeOfDecompression; + const strategy = kDecompress[encoding]; + + if (!strategy) { + throw new HttpieDecompressionError( + { + message: "DecompressionNotSupported", + buffer, + encodings, + response: this.response + }, + encoding + ); + } + + try { + decompressedBuffer = await strategy(decompressedBuffer); + } + catch (error) { + throw new HttpieDecompressionError({ + message: "UnexpectedDecompressionError", + buffer, + encodings, + error, + response: this.response + }); + } + } + + return decompressedBuffer; + } + + /** + * @description Parse Undici a buffer based on 'Content-Type' header. + * If the response as a content type equal to 'application/json' we automatically parse it with JSON.parse(). + */ + private async parseUndiciResponse(): Promise { + const buffer = await this.getDecompressedBuffer(); + const contentTypeHeader = this.response.headers["content-type"] as string; + + if (!contentTypeHeader) { + return buffer; + } + + let bodyAsString = ""; + try { + const { type, parameters } = contentType.parse(contentTypeHeader); + bodyAsString = buffer.toString(getEncodingCharset(parameters.charset)); + + if (type === "application/json") { + return JSON.parse(bodyAsString); + } + + if (type.startsWith("text/")) { + return bodyAsString; + } + } + catch (error) { + // Note: Even in case of an error we want to be able to recover the body that caused the JSON parsing error. + throw new HttpieParserError({ + message: "ResponseParsingError", + contentType: contentTypeHeader, + text: bodyAsString || null, + buffer, + error, + response: this.response + }); + } + + return buffer; + } +} diff --git a/src/common/errors.ts b/src/common/errors.ts new file mode 100644 index 0000000..922e1be --- /dev/null +++ b/src/common/errors.ts @@ -0,0 +1,53 @@ +/* eslint-disable max-len */ + +interface IGetErrorOptions { + error?: Error; + message: keyof T; +} + +// from myu-utils +function taggedString(chains: TemplateStringsArray, ...expectedValues: string[] | number[]) { + return function cur(...args: any[]): string { + const directory = args.at(-1) || {}; + const result = [chains[0]]; + expectedValues.forEach((key: string | number, index: number) => { + result.push( + typeof key === "number" ? args[key] : directory[key], + chains[index + 1] + ); + }); + + return result.join(""); + }; +} + +const kFetchBodyErrors = { + ResponseFetchError: taggedString`An unexpected error occurred while trying to retrieve the response body (reason: '${0}').` +}; + +const kDecompressionErrors = { + UnexpectedDecompressionError: taggedString`An unexpected error occurred when trying to decompress the response body (reason: '${0}').`, + DecompressionNotSupported: taggedString`Unsupported encoding '${0}'.` +}; + +const kParserErrors = { + ResponseParsingError: taggedString`An unexpected error occurred when trying to parse the response body (reason: '${0}').` +}; + +function getErrorsByType>>(errorDirectory: T) { + return (options: IGetErrorOptions, ...args: string[]) => { + const { error, message: errorLabel } = options; + const err = errorDirectory[errorLabel]; + + if (typeof err === "string") { + return err; + } + + return err(...(args.length === 0 ? [error?.message] : args)); + }; +} + +export const getFetchError = getErrorsByType(kFetchBodyErrors); +export const getDecompressionError = getErrorsByType(kDecompressionErrors); +export const getParserError = getErrorsByType(kParserErrors); + diff --git a/src/index.ts b/src/index.ts index 50d23ef..6d44c88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,9 @@ export * from "./stream"; export * from "./retry"; export * as policies from "./policies"; export { agents, computeURI, CustomHttpAgent } from "./agents"; -export { DEFAULT_HEADER } from "./utils"; +export { DEFAULT_HEADER, isHTTPError, isHttpieError } from "./utils"; + +export * from "./class/undiciResponseHandler"; export { Agent, diff --git a/src/request.ts b/src/request.ts index ab3fc2e..ac88a0b 100644 --- a/src/request.ts +++ b/src/request.ts @@ -1,6 +1,6 @@ // Import Node.js Dependencies -import { IncomingHttpHeaders } from "http"; -import { URLSearchParams } from "url"; +import { IncomingHttpHeaders } from "node:http"; +import { URLSearchParams } from "node:url"; // Import Third-party Dependencies import * as undici from "undici"; @@ -10,6 +10,8 @@ import status from "statuses"; // Import Internal Dependencies import * as Utils from "./utils"; import { computeURI } from "./agents"; +import { HttpieResponseHandler, ModeOfHttpieResponseHandler } from "./class/undiciResponseHandler"; +import { HttpieOnHttpError } from "./class/HttpieOnHttpError"; export type WebDavMethod = "MKCOL" | "COPY" | "MOVE" | "LOCK" | "UNLOCK" | "PROPFIND" | "PROPPATCH"; export type HttpMethod = "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE" | "PATCH" ; @@ -23,17 +25,21 @@ export interface RequestError extends Error { } export interface RequestOptions { - /** Default: 0 */ + /** @default 0 */ maxRedirections?: number; - /** Default: { "user-agent": "httpie" } */ + /** @default{ "user-agent": "httpie" } */ headers?: IncomingHttpHeaders; querystring?: string | URLSearchParams; body?: any; authorization?: string; // Could be dynamically computed depending on the provided URI. agent?: undici.Agent | undici.ProxyAgent | undici.MockAgent; - // API limiter from a package like "p-ratelimit" + /** @description API limiter from a package like `p-ratelimit`. */ limit?: InlineCallbackAction; + /** @default "parse" */ + mode?: ModeOfHttpieResponseHandler; + /** @default true */ + throwOnHttpError?: boolean; } export interface RequestResponse { @@ -78,18 +84,26 @@ export async function request( await limit(() => undici.request(computedURI.url, requestOptions)); const statusCode = requestResponse.statusCode; + const responseHandler = new HttpieResponseHandler(requestResponse); + + let data; + if (options.mode === "parse" || !options.mode) { + data = await responseHandler.getData("parse"); + } + else { + data = await responseHandler.getData(options.mode); + } + const RequestResponse = { headers: requestResponse.headers, statusMessage: status.message[requestResponse.statusCode]!, statusCode, - data: void 0 as any + data }; - const data = await Utils.parseUndiciResponse(requestResponse); - RequestResponse.data = data; - - if (statusCode >= 400) { - throw Utils.toError(RequestResponse); + const shouldThrowOnHttpError = options.throwOnHttpError ?? true; + if (shouldThrowOnHttpError && statusCode >= 400) { + throw new HttpieOnHttpError(RequestResponse); } return RequestResponse; diff --git a/src/stream.ts b/src/stream.ts index 19c5732..451130d 100644 --- a/src/stream.ts +++ b/src/stream.ts @@ -1,5 +1,5 @@ // Import Node.js Dependencies -import { Duplex } from "stream"; +import { Duplex } from "node:stream"; // Import Third-party Dependencies import * as undici from "undici"; diff --git a/src/utils.ts b/src/utils.ts index fb5cb6c..c427125 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,25 +1,19 @@ /* eslint-disable no-redeclare */ // Import Node.js Dependencies -import { gunzip } from "node:zlib"; -import { promisify } from "node:util"; import { IncomingHttpHeaders } from "node:http"; -// Import Third-party Dependencies -import * as contentType from "content-type"; -import { Dispatcher } from "undici"; - // Import Internal Dependencies -import { RequestResponse, RequestOptions } from "./request"; +import { RequestOptions, RequestResponse } from "./request"; +import { HttpieError } from "./class/HttpieCommonError"; +import { HttpieOnHttpError } from "./class/HttpieOnHttpError"; // CONSTANTS -const kDefaultMimeType = "text/plain"; const kDefaultUserAgent = "httpie"; const kDefaultEncodingCharset = "utf-8"; const kCharsetConversionTable = { "ISO-8859-1": "latin1" }; -const kAsyncGunzip = promisify(gunzip); export const DEFAULT_HEADER = { "user-agent": kDefaultUserAgent }; @@ -39,36 +33,6 @@ export function getEncodingCharset(charset = kDefaultEncodingCharset): BufferEnc return charset in kCharsetConversionTable ? kCharsetConversionTable[charset] : "utf-8"; } -/** - * @description Parse Undici ResponseData (the body is a Node.js Readable Stream). - * If the response as a content type equal to 'application/json' we automatically parse it with JSON.parse(). - */ -export async function parseUndiciResponse(response: Dispatcher.ResponseData): Promise { - const contentTypeHeader = response.headers["content-type"] as string | undefined; - const { type, parameters } = contentType.parse( - contentTypeHeader ?? kDefaultMimeType - ); - - let body; - try { - if (response.headers["content-encoding"] === "gzip") { - const buf = await response.body.arrayBuffer(); - body = (await kAsyncGunzip(buf)).toString(getEncodingCharset(parameters.charset)); - } - else { - body = await response.body.text(); - } - - return type === "application/json" && body ? JSON.parse(body) : body; - } - catch (error) { - // Note: Even in case of an error we want to be able to recover the body that caused the JSON parsing error. - error.body = body; - - throw error; - } -} - /** * @description Create a default plain Object headers that will contains a Set of default values like: * - User-agent @@ -83,6 +47,15 @@ export function createHeaders(options: Partial = RequestResponse>(error: unknown): error is HttpieOnHttpError { + return error instanceof HttpieOnHttpError; +} + export function createBody(body: undefined): undefined; export function createBody(body: any, headers?: IncomingHttpHeaders): string | Buffer; @@ -111,20 +84,6 @@ export function createBody(body: any, headers: IncomingHttpHeaders = {}): string return finalBody; } -/** - * @description Helpers function to generate an Error with all the required properties from the response. - * We attach them to the error so that they can be retrieved by the developer in a Catch block. - */ -export function toError(response: RequestResponse) { - const err = new Error(response.statusMessage) as Error & RequestResponse; - err.statusMessage = response.statusMessage; - err.statusCode = response.statusCode; - err.headers = response.headers; - err.data = response.data; - - return err; -} - /** * @description Helpers to generate a Basic or Bearer token for the HTTP Authorization header. * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization diff --git a/test/HttpieOnHttpError.spec.ts b/test/HttpieOnHttpError.spec.ts new file mode 100644 index 0000000..317911b --- /dev/null +++ b/test/HttpieOnHttpError.spec.ts @@ -0,0 +1,41 @@ +// Import Third-Party Dependencies +import { MockAgent, setGlobalDispatcher } from "undici"; + +// Import Internal Dependencies +import { request } from "../src/request"; + +describe("HttpieOnHttpError", () => { + it("it should create an HttpieOnHttpError with the properties of RequestResponse", async() => { + expect.assertions(2); + + const expectedResponseData = { + statusCode: 404, + statusMessage: "Not Found", + data: "La data.", + headers: { "content-type": "text/plain" } + }; + const url = "http://test.com"; + const targetUrl = { + method: "GET", + path: "/test" + }; + const path = url + targetUrl.path; + + const mockAgent = new MockAgent(); + setGlobalDispatcher(mockAgent); + mockAgent.disableNetConnect(); + + const pool = mockAgent.get(url); + pool.intercept(targetUrl).reply(expectedResponseData.statusCode, expectedResponseData.data, { + headers: expectedResponseData.headers + }); + + try { + await request(targetUrl.method as any, path); + } + catch (error) { + expect(error.name).toStrictEqual("HttpieOnHttpError"); + expect(error).toMatchObject(expectedResponseData); + } + }); +}); diff --git a/test/request.spec.ts b/test/request.spec.ts index 829b23c..2d06a84 100644 --- a/test/request.spec.ts +++ b/test/request.spec.ts @@ -87,21 +87,25 @@ describe("http.get", () => { await get("/windev/hlkezcjcke"); } catch (error) { - expect(error.name).toStrictEqual("Error"); + expect(error.name).toStrictEqual("HttpieOnHttpError"); expect(error.statusCode).toStrictEqual(404); expect(error.statusMessage).toStrictEqual("Not Found"); expect(error.data).toMatchSnapshot(); } }); - it("should throw a 'SyntaxError' with jsonError endpoint from local fastify server", async() => { - expect.assertions(1); + it("should throw a 'HttpieParserError' with jsonError endpoint from local fastify server", async() => { + expect.assertions(4); + const expectedPayload = "{ 'foo': bar }"; try { await get("/local/jsonError"); } catch (error) { - expect(error.name).toStrictEqual("SyntaxError"); + expect(error.name).toStrictEqual("ResponseParsingError"); + expect(error.reason.name).toStrictEqual("SyntaxError"); + expect(error.text).toStrictEqual(expectedPayload); + expect(error.buffer).toStrictEqual(Buffer.from(expectedPayload)); } }); }); @@ -179,7 +183,7 @@ describe("http.safeGet", () => { if (result.err) { const error = result.val; - expect(error.name).toStrictEqual("Error"); + expect(error.name).toStrictEqual("HttpieOnHttpError"); expect(error.statusCode).toStrictEqual(404); expect(error.statusMessage).toStrictEqual("Not Found"); expect(error.data).toMatchSnapshot(); diff --git a/test/server/index.ts b/test/server/index.ts index b46b733..097cf90 100644 --- a/test/server/index.ts +++ b/test/server/index.ts @@ -1,7 +1,7 @@ // Import Node.js Dependencies -import path from "path"; -import fs from "fs"; -import { Transform } from "stream"; +import path from "node:path"; +import fs from "node:fs"; +import { Transform } from "node:stream"; // Import Third-party Dependencies import fastify from "fastify"; diff --git a/test/stream.spec.ts b/test/stream.spec.ts index 710001e..d1e093f 100644 --- a/test/stream.spec.ts +++ b/test/stream.spec.ts @@ -1,11 +1,11 @@ +// Import Node.js Dependencies +import { createWriteStream, createReadStream, existsSync, promises as fs } from "node:fs"; +import path from "node:path"; +import { pipeline } from "node:stream/promises"; + // Import Third-party Dependencies import { FastifyInstance } from "fastify"; -// Import Node.js Dependencies -import { createWriteStream, createReadStream, existsSync, promises as fs } from "fs"; -import path from "path"; -import { pipeline } from "stream/promises"; - // Import Internal Dependencies import * as httpie from "../src/index"; import { createServer } from "./server/index"; diff --git a/test/undiciResponseHandler.spec.ts b/test/undiciResponseHandler.spec.ts new file mode 100644 index 0000000..06e8d3d --- /dev/null +++ b/test/undiciResponseHandler.spec.ts @@ -0,0 +1,298 @@ +/* eslint-disable max-len */ + +// Import Node.js Dependencies +import { randomBytes } from "node:crypto"; + +// Import Third-party Dependencies +import { brotliCompressSync, deflateSync, gzipSync } from "zlib"; + +// Import Internal Dependencies +import { HttpieResponseHandler } from "../src/class/undiciResponseHandler"; + +function toArrayBuffer(buffer: Buffer) { + const { byteOffset, byteLength } = buffer; + + return buffer.buffer.slice(byteOffset, byteOffset + byteLength); +} + +describe("HttpieResponseHandler.getData", () => { + it("should return the parsed payload by default", async() => { + const payload = { foo: "bar" }; + const mockResponse = { + body: { arrayBuffer: () => toArrayBuffer(Buffer.from(JSON.stringify(payload))) }, + headers: { "content-type": "application/json" } + }; + const handler = new HttpieResponseHandler(mockResponse as any); + const data = await handler.getData(); + + expect(data).toMatchObject(payload); + }); +}); + +describe("HttpieResponseHandler.getData (mode: 'raw')", () => { + it("should return the rawBuffer", async() => { + const payload = Buffer.from(JSON.stringify({ foo: "bar" })); + const mockResponse = { + body: { arrayBuffer: () => toArrayBuffer(payload) }, + headers: { "content-type": "application/json" } + }; + const handler = new HttpieResponseHandler(mockResponse as any); + const data = await handler.getData("raw"); + + expect(data).toMatchObject(payload); + }); + + it("should throw HttpieFetchBodyError", async() => { + expect.assertions(4); + + const errMsg = "unexpected error"; + const mockResponse = { + statusCode: 200, + body: { + arrayBuffer: () => { + throw new Error(errMsg); + } + }, + headers: { "content-type": "application/json" } + }; + const handler = new HttpieResponseHandler(mockResponse as any); + try { + await handler.getData(); + } + catch (error) { + expect(error.name).toStrictEqual("ResponseFetchError"); + expect(error.message).toStrictEqual(`An unexpected error occurred while trying to retrieve the response body (reason: '${errMsg}').`); + expect(error.statusCode).toStrictEqual(mockResponse.statusCode); + expect(error.headers).toStrictEqual(mockResponse.headers); + } + }); +}); + +describe("HttpieResponseHandler.getData (mode: 'decompress')", () => { + it("must returns the original buffer when there is no 'content-encoding'", async() => { + const buf = Buffer.from("hello world!"); + const mockResponse = { + body: { arrayBuffer: () => toArrayBuffer(buf) }, + headers: {} + }; + + const handler = new HttpieResponseHandler(mockResponse as any); + const data = await handler.getData("decompress"); + + expect(data).toStrictEqual(buf); + }); + + it("must throw when the 'content-encoding' header is set with an unknown value", async() => { + expect.assertions(6); + + const buf = Buffer.from("hello world!"); + const encoding = randomBytes(4).toString("hex"); + const mockResponse = { + statusCode: 200, + body: { arrayBuffer: () => toArrayBuffer(buf) }, + headers: { "content-encoding": encoding } + }; + const handler = new HttpieResponseHandler(mockResponse as any); + + try { + await handler.getData("decompress"); + } + catch (error) { + expect(error.message).toStrictEqual(`Unsupported encoding '${encoding}'.`); + expect(error.buffer).toStrictEqual(buf); + expect(error.encodings).toStrictEqual([encoding]); + expect(error.name).toStrictEqual("DecompressionNotSupported"); + expect(error.statusCode).toStrictEqual(mockResponse.statusCode); + expect(error.headers).toStrictEqual(mockResponse.headers); + } + }); + + it("must throw when the 'content-encoding' header is a list that includes an unknown value", async() => { + expect.assertions(6); + + const buf = Buffer.from("hello world!"); + const encoding = randomBytes(4).toString("hex"); + const mockResponse = { + statusCode: 200, + body: { arrayBuffer: () => toArrayBuffer(buf) }, + headers: { "content-encoding": [encoding] } + }; + const handler = new HttpieResponseHandler(mockResponse as any); + + try { + await handler.getData("decompress"); + } + catch (error) { + expect(error.message).toStrictEqual(`Unsupported encoding '${encoding}'.`); + expect(error.buffer).toStrictEqual(buf); + expect(error.encodings).toStrictEqual([encoding]); + expect(error.name).toStrictEqual("DecompressionNotSupported"); + expect(error.statusCode).toStrictEqual(mockResponse.statusCode); + expect(error.headers).toStrictEqual(mockResponse.headers); + } + }); + + it("must use 'gunzip' before to returning an uncompressed buffer when the 'content-encoding' header is set with 'gzip'", async() => { + const buf = Buffer.from("hello world!"); + const mockResponse = { + body: { arrayBuffer: () => toArrayBuffer(gzipSync(buf)) }, + headers: { "content-encoding": "gzip" } + }; + const handler = new HttpieResponseHandler(mockResponse as any); + const data = await handler.getData("decompress"); + + expect(data).toStrictEqual(buf); + }); + + it("must use 'gunzip' before to returning an uncompressed buffer when the 'content-encoding' header is set with 'x-gzip'", async() => { + const buf = Buffer.from("hello world!"); + const mockResponse = { + body: { arrayBuffer: () => toArrayBuffer(gzipSync(buf)) }, + headers: { "content-encoding": "x-gzip" } + }; + const handler = new HttpieResponseHandler(mockResponse as any); + const data = await handler.getData("decompress"); + + expect(data).toStrictEqual(buf); + }); + + it("must use 'brotliDecompress' before to returning an uncompressed buffer when the 'content-encoding' header is set with 'br'", async() => { + const buf = Buffer.from("hello world!"); + const mockResponse = { + body: { arrayBuffer: () => toArrayBuffer(brotliCompressSync(buf)) }, + headers: { "content-encoding": "br" } + }; + const handler = new HttpieResponseHandler(mockResponse as any); + const data = await handler.getData("decompress"); + + expect(data).toStrictEqual(buf); + }); + + it("must use 'inflate' before to returning an uncompressed buffer when the 'content-encoding' header is set with 'deflate'", async() => { + const buf = Buffer.from("hello world!"); + const mockResponse = { + body: { arrayBuffer: () => toArrayBuffer(deflateSync(buf)) }, + headers: { "content-encoding": "deflate" } + }; + const handler = new HttpieResponseHandler(mockResponse as any); + const data = await handler.getData("decompress"); + + expect(data).toStrictEqual(buf); + }); + + it("must decompress in reverse order of the given encodings list when there are multiple compression types", async() => { + const buf = Buffer.from("hello world!"); + const encodings = ["deflate", "gzip"]; + const mockResponse = { + body: { arrayBuffer: () => toArrayBuffer(gzipSync(deflateSync(buf))) }, + headers: { "content-encoding": encodings } + }; + const handler = new HttpieResponseHandler(mockResponse as any); + const data = await handler.getData("decompress"); + + expect(data).toStrictEqual(buf); + }); + + it("must decompress in reverse order of the given encodings string when there are multiple compression types", async() => { + const buf = Buffer.from("hello world!"); + const encodings = "deflate, gzip"; + const mockResponse = { + body: { arrayBuffer: () => toArrayBuffer(gzipSync(deflateSync(buf))) }, + headers: { "content-encoding": encodings } + }; + const handler = new HttpieResponseHandler(mockResponse as any); + const data = await handler.getData("decompress"); + + expect(data).toStrictEqual(buf); + }); +}); + +describe("HttpieResponseHandler.getData (mode: 'parse')", () => { + it("should parse a JSON response with no errors", async() => { + const payload = { foo: "bar" }; + const mockResponse = { + body: { arrayBuffer: () => toArrayBuffer(Buffer.from(JSON.stringify(payload))) }, + headers: { "content-type": "application/json" } + }; + const handler = new HttpieResponseHandler(mockResponse as any); + const data = await handler.getData("parse"); + + expect(data).toMatchObject(payload); + }); + + it("should parse an invalid JSON response but still keep the request data in the Error", async() => { + expect.assertions(5); + + const payload = "{\"foo\": bar}"; + const buf = Buffer.from("{\"foo\": bar}"); + + const mockResponse = { + statusCode: 200, + body: { arrayBuffer: () => toArrayBuffer(Buffer.from(payload)) }, + headers: { "content-type": "application/json" } + }; + const handler = new HttpieResponseHandler(mockResponse as any); + try { + await handler.getData("parse"); + } + catch (error) { + expect(error.text).toStrictEqual(payload); + expect(error.buffer).toStrictEqual(buf); + expect(error.name).toStrictEqual("ResponseParsingError"); + expect(error.statusCode).toStrictEqual(mockResponse.statusCode); + expect(error.headers).toMatchObject(mockResponse.headers); + } + }); + + it("should return the original buffer when there is no content-type", async() => { + const payload = Buffer.from("hello world!"); + // const data = await HttpieResponseHandler.parseUndiciResponse(payload); + + const mockResponse = { + body: { arrayBuffer: () => toArrayBuffer(payload) }, + headers: {} + }; + const handler = new HttpieResponseHandler(mockResponse as any); + const data = await handler.getData("parse"); + + expect(data).toStrictEqual(payload); + }); + + it("must converting it to a string when the 'content-type' header starts with 'text/'", async() => { + const payload = "hello world!"; + const mockResponse = { + body: { arrayBuffer: () => toArrayBuffer(Buffer.from(payload)) }, + headers: { "content-type": "text/anything" } + }; + const handler = new HttpieResponseHandler(mockResponse as any); + const data = await handler.getData("parse"); + + expect(data).toStrictEqual(payload); + }); + + it("must converting body to JSON when the 'content-type' header is set with 'application/json'", async() => { + const payload = { foo: "hello world!" }; + + const mockResponse = { + body: { arrayBuffer: () => toArrayBuffer(Buffer.from(JSON.stringify(payload))) }, + headers: { "content-type": "application/json; charset=utf-8" } + }; + const handler = new HttpieResponseHandler(mockResponse as any); + const data = await handler.getData("parse"); + + expect(data).toStrictEqual(payload); + }); + + it("must return the original buffer when 'content-type' header is set with 'application/pdf'", async() => { + const buf = Buffer.from("hello world!"); + const mockResponse = { + body: { arrayBuffer: () => toArrayBuffer(buf) }, + headers: { "content-type": "application/pdf" } + }; + + const handler = new HttpieResponseHandler(mockResponse as any); + const data = await handler.getData("parse"); + + expect(data).toStrictEqual(buf); + }); +}); diff --git a/test/utils.spec.ts b/test/utils.spec.ts index faba545..5c25a10 100644 --- a/test/utils.spec.ts +++ b/test/utils.spec.ts @@ -1,10 +1,11 @@ // Import Node.js Dependencies -import { IncomingHttpHeaders } from "http2"; -import { gzipSync } from "zlib"; -import stream from "stream"; +import { IncomingHttpHeaders } from "node:http2"; +import stream from "node:stream"; // Import Internal Dependencies import * as Utils from "../src/utils"; +import { HttpieOnHttpError } from "../src/class/HttpieOnHttpError"; +import { HttpieDecompressionError, HttpieFetchBodyError, HttpieParserError } from "../src/class/HttpieHandlerError"; describe("isAsyncIterable", () => { it("should return false for synchronous iterable like an Array", () => { @@ -140,131 +141,34 @@ describe("createAuthorizationHeader", () => { }); }); -describe("toError", () => { - it("it should create an Error with the properties of RequestResponse", () => { - const reqResponse = { - statusCode: 404, - statusMessage: "Not Found", - data: null, - headers: {} - }; - - const error = Utils.toError(reqResponse); - expect(error.name).toStrictEqual("Error"); - expect(error).toMatchObject(reqResponse); - }); -}); - -describe("parseUndiciResponse", () => { - const defaultUndiciResponseMeta = { - statusCode: 200, - context: {}, - opaque: null, - trailers: {} - }; - - it("should parse a JSON response with no errors", async() => { - const payload = JSON.stringify({ foo: "bar" }); - const body: any = { - text() { - return Promise.resolve(payload); - } - }; - - const data = await Utils.parseUndiciResponse<{ foo: string }>({ - ...defaultUndiciResponseMeta, body, - headers: { - "content-type": "application/json" - } - }); - - expect(data).toMatchObject({ foo: "bar" }); - }); - - it("should parse an invalid JSON response but still keep the body string in the Error", async() => { - expect.assertions(1); - - const payload = "{\"foo\": bar}"; - const body: any = { - text() { - return Promise.resolve(payload); - } - }; - - try { - await Utils.parseUndiciResponse<{ foo: string }>({ - ...defaultUndiciResponseMeta, body, - headers: { - "content-type": "application/json" - } - }); - } - catch (error) { - expect(error.body).toStrictEqual(payload); - } - }); - - it("should parse the response as a plain/text", async() => { - const payload = "hello world!"; - const body: any = { - text() { - return Promise.resolve(payload); - } - }; - - const data = await Utils.parseUndiciResponse({ - ...defaultUndiciResponseMeta, body, headers: {} - }); - expect(data).toStrictEqual(payload); +describe("isHttpieError", () => { + it("it should be true", () => { + expect(Utils.isHttpieError(new HttpieOnHttpError({} as any))).toBeTruthy(); + expect(Utils.isHttpieError(new HttpieFetchBodyError({ message: "ResponseFetchError", response: {} } as any))).toBeTruthy(); + expect( + Utils.isHttpieError(new HttpieDecompressionError({ message: "UnexpectedDecompressionError", response: {} } as any)) + ).toBeTruthy(); + expect(Utils.isHttpieError(new HttpieParserError({ message: "ResponseParsingError", response: {} } as any))).toBeTruthy(); }); - it("must unzip data when there is a 'content-encoding' header set with 'gzip' before to converting it to a string", async() => { - const payload = "hello world!"; - const body: any = { - async arrayBuffer() { - return gzipSync(payload); - } - }; - const data = await Utils.parseUndiciResponse({ - ...defaultUndiciResponseMeta, body, headers: { - "content-encoding": "gzip" - } - }); - - expect(data).toStrictEqual(payload); + it("it should be false", () => { + expect(Utils.isHttpieError(new Error())).toBeFalsy(); }); +}); - it("must unzip data when there is a 'content-encoding' header set with 'gzip' before to converting it to JSON", async() => { - const payload = { foo: "hello world!" }; - const body: any = { - async arrayBuffer() { - return gzipSync(JSON.stringify(payload)); - } - }; - - const data = await Utils.parseUndiciResponse({ - ...defaultUndiciResponseMeta, body, headers: { - "content-encoding": "gzip", - "content-type": "application/json; charset=utf-8" - } - }); - - expect(data).toStrictEqual(payload); +describe("isHTTPError", () => { + it("it should be true", () => { + expect(Utils.isHTTPError(new HttpieOnHttpError({} as any))).toBeTruthy(); }); - it("should not unzip data when 'content-encoding' header is not set", async() => { - const payload = "hello world!"; - const buf = gzipSync(payload); - const body: any = { - text: () => buf.toString() - }; - - const data = await Utils.parseUndiciResponse({ - ...defaultUndiciResponseMeta, body, headers: {} - }); - - expect(data).toStrictEqual(buf.toString()); + it("it should be false", () => { + expect(Utils.isHTTPError(new Error())).toBeFalsy(); + expect(Utils.isHTTPError(new HttpieFetchBodyError({ message: "ResponseFetchError", response: {} } as any))).toBeFalsy(); + expect( + Utils.isHTTPError(new HttpieDecompressionError({ message: "UnexpectedDecompressionError", response: {} } as any)) + ).toBeFalsy(); + expect(Utils.isHTTPError(new HttpieParserError({ message: "ResponseParsingError", response: {} } as any))).toBeFalsy(); }); }); From 58eba99652cc5bbf53238375d78f7969a7525f9f Mon Sep 17 00:00:00 2001 From: SofianD Date: Fri, 9 Feb 2024 16:54:52 +0100 Subject: [PATCH 2/9] fix RequestError type --- src/request.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/request.ts b/src/request.ts index ac88a0b..986b14d 100644 --- a/src/request.ts +++ b/src/request.ts @@ -12,17 +12,19 @@ import * as Utils from "./utils"; import { computeURI } from "./agents"; import { HttpieResponseHandler, ModeOfHttpieResponseHandler } from "./class/undiciResponseHandler"; import { HttpieOnHttpError } from "./class/HttpieOnHttpError"; +import { HttpieError } from "./class/HttpieCommonError"; +import { HttpieDecompressionError, HttpieFetchBodyError, HttpieParserError } from "./class/HttpieHandlerError"; export type WebDavMethod = "MKCOL" | "COPY" | "MOVE" | "LOCK" | "UNLOCK" | "PROPFIND" | "PROPPATCH"; export type HttpMethod = "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE" | "PATCH" ; export type InlineCallbackAction = (fn: () => Promise) => Promise; -export interface RequestError extends Error { - statusMessage: string; - statusCode: number; - headers: IncomingHttpHeaders; - data: E; -} +export type RequestError = + Partial>> & + Partial & + Partial & + Partial & + HttpieError; export interface RequestOptions { /** @default 0 */ From bbc22e1809ccd651310ed02202b0ef1fa1612ade Mon Sep 17 00:00:00 2001 From: SofianD Date: Fri, 9 Feb 2024 16:55:09 +0100 Subject: [PATCH 3/9] remove I --- src/class/HttpieCommonError.ts | 4 ++-- src/class/HttpieHandlerError.ts | 17 +++++++++-------- src/class/HttpieOnHttpError.ts | 2 +- src/common/errors.ts | 4 ++-- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/class/HttpieCommonError.ts b/src/class/HttpieCommonError.ts index 9895eb9..63844f7 100644 --- a/src/class/HttpieCommonError.ts +++ b/src/class/HttpieCommonError.ts @@ -6,7 +6,7 @@ type CommonResponseData = { headers: IncomingHttpHeaders; } -export interface IHttpieErrorOptions { +export interface HttpieErrorOptions { response: CommonResponseData; } @@ -14,7 +14,7 @@ export class HttpieError extends Error { headers: IncomingHttpHeaders; statusCode: number; - constructor(message: string, options: IHttpieErrorOptions) { + constructor(message: string, options: HttpieErrorOptions) { super(message); this.statusCode = options.response.statusCode; diff --git a/src/class/HttpieHandlerError.ts b/src/class/HttpieHandlerError.ts index a755ab6..81e6089 100644 --- a/src/class/HttpieHandlerError.ts +++ b/src/class/HttpieHandlerError.ts @@ -1,16 +1,17 @@ /* eslint-disable max-classes-per-file */ // Import Third-party Dependencies -import { HttpieError, IHttpieErrorOptions } from "./HttpieCommonError"; +import { HttpieError, HttpieErrorOptions } from "./HttpieCommonError"; import { getDecompressionError, getFetchError, getParserError } from "../common/errors"; -interface IHttpieHandlerError extends IHttpieErrorOptions { +interface HttpieHandlerErrorOptions extends HttpieErrorOptions { /** @description original error */ error?: Error; message: T; } -interface IHttpieDecompressionErrorOptions extends IHttpieHandlerError[0]["message"]> { +// eslint-disable-next-line max-len +interface HttpieDecompressionErrorOptions extends HttpieHandlerErrorOptions[0]["message"]> { /** @description original body as buffer */ buffer: Buffer; /** @description encodings from 'content-encoding' header */ @@ -18,7 +19,7 @@ interface IHttpieDecompressionErrorOptions extends IHttpieHandlerError[0]["message"]> { +interface HttpieParserErrorOptions extends HttpieHandlerErrorOptions[0]["message"]> { /** @description content-type from 'content-type' header without params */ contentType: string; /** @description original body as buffer */ @@ -30,7 +31,7 @@ interface IHttpieParserErrorOptions extends IHttpieHandlerError[0]["message"]>, ...args) { + constructor(options: HttpieHandlerErrorOptions[0]["message"]>, ...args) { super(getFetchError(options, ...args), options); } } @@ -48,7 +49,7 @@ export class HttpieDecompressionError extends HttpieHandlerError { buffer: Buffer; encodings: string[]; - constructor(options: IHttpieDecompressionErrorOptions, ...args) { + constructor(options: HttpieDecompressionErrorOptions, ...args) { super(getDecompressionError(options, ...args), options); this.buffer = options.buffer; @@ -61,7 +62,7 @@ export class HttpieParserError extends HttpieHandlerError { buffer: Buffer; text: string | null; - constructor(options: IHttpieParserErrorOptions, ...args) { + constructor(options: HttpieParserErrorOptions, ...args) { super(getParserError(options, ...args), options); this.buffer = options.buffer; diff --git a/src/class/HttpieOnHttpError.ts b/src/class/HttpieOnHttpError.ts index 3ebc98a..80307e4 100644 --- a/src/class/HttpieOnHttpError.ts +++ b/src/class/HttpieOnHttpError.ts @@ -10,7 +10,7 @@ export class HttpieOnHttpError> extends HttpieErr name = "HttpieOnHttpError"; statusMessage: string; - data: T; + data: T["data"]; constructor(response: T) { super(response.statusMessage, { response }); diff --git a/src/common/errors.ts b/src/common/errors.ts index 922e1be..30d3331 100644 --- a/src/common/errors.ts +++ b/src/common/errors.ts @@ -1,6 +1,6 @@ /* eslint-disable max-len */ -interface IGetErrorOptions { +interface GetErrorOptions { error?: Error; message: keyof T; } @@ -35,7 +35,7 @@ const kParserErrors = { }; function getErrorsByType>>(errorDirectory: T) { - return (options: IGetErrorOptions, ...args: string[]) => { + return (options: GetErrorOptions, ...args: string[]) => { const { error, message: errorLabel } = options; const err = errorDirectory[errorLabel]; From f85c20f6b4ea01180dc70a54938b150ff4798b6e Mon Sep 17 00:00:00 2001 From: SofianD Date: Thu, 22 Feb 2024 17:13:26 +0100 Subject: [PATCH 4/9] use undici MockAgent to test request and safeRequest --- test/request2.spec.ts | 2025 +++++++++++++++++++++++++++++++++++ test/safeRequest.spec.ts | 2175 ++++++++++++++++++++++++++++++++++++++ test/server/index.ts | 15 + 3 files changed, 4215 insertions(+) create mode 100644 test/request2.spec.ts create mode 100644 test/safeRequest.spec.ts diff --git a/test/request2.spec.ts b/test/request2.spec.ts new file mode 100644 index 0000000..5a1ff36 --- /dev/null +++ b/test/request2.spec.ts @@ -0,0 +1,2025 @@ +/* eslint-disable max-lines */ +// Import Third-Party Dependencies +import { Interceptable, MockAgent, setGlobalDispatcher } from "undici"; + +// Import Internal Dependencies +import { request } from "../src/request"; +import { brotliCompress, deflate, gzip } from "zlib"; +import { promisify } from "util"; +import { isHTTPError, isHttpieError } from "../src"; +import { randomInt } from "crypto"; + +// CONSTANTS +const kUrl = "http://test.com"; +const kAsyncGzip = promisify(gzip); +const kAsyncBrotli = promisify(brotliCompress); +const kAsyncDeflate = promisify(deflate); + +// VARS +let pool: Interceptable; + +describe("Httpie.safeRequest", () => { + beforeAll(() => { + const mockAgent = new MockAgent(); + setGlobalDispatcher(mockAgent); + mockAgent.disableNetConnect(); + + pool = mockAgent.get(kUrl); + }); + + describe("with ThrowOnHttpError", () => { + describe("GET", () => { + it("should throw if the response status code is higher than 400", async() => { + expect.assertions(4); + + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = randomInt(400, 503); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path); + } + catch (error) { + expect(isHTTPError(error)).toBeTruthy(); + expect(error.statusCode).toBe(statusCode); + expect(error.data).toBe(payload.toString()); + expect(error.headers).toMatchObject(headers); + } + }); + + it("should not throw if the response status code is lower than 400", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = randomInt(200, 399); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + }); + + describe("POST", () => { + it("should throw if the response status code is higher than 400", async() => { + expect.assertions(4); + + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = randomInt(400, 503); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path); + } + catch (error) { + expect(isHTTPError(error)).toBeTruthy(); + expect(error.statusCode).toBe(statusCode); + expect(error.data).toBe(payload.toString()); + expect(error.headers).toMatchObject(headers); + } + }); + + it("should not throw if the response status code is lower than 400", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = randomInt(200, 399); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + }); + + describe("PUT", () => { + it("should throw if the response status code is higher than 400", async() => { + expect.assertions(4); + + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = randomInt(400, 503); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path); + } + catch (error) { + expect(isHTTPError(error)).toBeTruthy(); + expect(error.statusCode).toBe(statusCode); + expect(error.data).toBe(payload.toString()); + expect(error.headers).toMatchObject(headers); + } + }); + + it("should not throw if the response status code is lower than 400", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = randomInt(200, 399); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + }); + + describe("DELETE", () => { + it("should throw if the response status code is higher than 400", async() => { + expect.assertions(4); + + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = randomInt(400, 503); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path); + } + catch (error) { + expect(isHTTPError(error)).toBeTruthy(); + expect(error.statusCode).toBe(statusCode); + expect(error.data).toBe(payload.toString()); + expect(error.headers).toMatchObject(headers); + } + }); + + it("should not throw if the response status code is lower than 400", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = randomInt(200, 399); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + }); + }); + + describe("without ThrowOnHttpError", () => { + describe("GET", () => { + it("should not throw if the response status code is higher than 400", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = randomInt(400, 503); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { throwOnHttpError: false }); + + + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + + it("should not throw if the response status code is lower than 400", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = randomInt(200, 399); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { throwOnHttpError: false }); + + + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + }); + + describe("POST", () => { + it("should not throw if the response status code is higher than 400", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = randomInt(400, 503); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { throwOnHttpError: false }); + + + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + + it("should not throw if the response status code is lower than 400", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = randomInt(200, 399); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { throwOnHttpError: false }); + + + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + }); + + describe("PUT", () => { + it("should not throw if the response status code is higher than 400", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = randomInt(400, 503); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { throwOnHttpError: false }); + + + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + + it("should not throw if the response status code is lower than 400", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = randomInt(200, 399); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { throwOnHttpError: false }); + + + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + }); + + describe("DELETE", () => { + it("should not throw if the response status code is higher than 400", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = randomInt(400, 503); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { throwOnHttpError: false }); + + + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + + it("should not throw if the response status code is lower than 400", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = randomInt(200, 399); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { throwOnHttpError: false }); + + + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + }); + }); + + describe("PARSE mode (default)", () => { + describe("GET", () => { + it("should return a parsed response as text when 'content-type' header starts with 'text/'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = "La data."; + const buf = Buffer.from(payload); + + pool.intercept(target).reply(statusCode, buf, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a parsed response as object when 'content-type' header is set with 'application/json'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/json" }; + const payload = { my: "object" }; + const buf = Buffer.from(JSON.stringify(payload)); + + pool.intercept(target).reply(statusCode, buf, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a buffer when 'content-type' header is set with 'application/pdf'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/pdf" }; + const payload = Buffer.from("mon pdf"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(Buffer.from(payload)); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a buffer when 'content-type' header is set with unsupported value", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/msword" }; + const payload = Buffer.from(JSON.stringify({ my: "object" })); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should throw when 'content-type' header is set with unsupported value", async() => { + expect.assertions(5); + + const target = { + method: "GET", + path: "/test" + }; + const statusCode = 200; + const headers = { "content-type": "unknown" }; + const payload = Buffer.from(JSON.stringify({ my: "object" })); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path); + } + catch (error) { + expect(isHttpieError(error)).toBeTruthy(); + expect(error.message).toStrictEqual( + "An unexpected error occurred when trying to parse the response body (reason: 'invalid media type')." + ); + expect(error.buffer).toStrictEqual(payload); + expect(error.headers).toMatchObject(headers); + expect(error.statusCode).toBe(200); + } + }); + + it("should throw when 'content-encoding' header is set with unsupported value", async() => { + expect.assertions(5); + + const target = { + method: "GET", + path: "/test" + }; + + const payload = await kAsyncGzip("Mon document"); + const headers = { "content-encoding": "unknown" }; + const statusCode = 200; + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path); + } + catch (error) { + expect(error.message).toStrictEqual("Unsupported encoding 'unknown'."); + expect(error.buffer).toStrictEqual(payload); + expect(error.headers).toMatchObject(headers); + expect(error.statusCode).toBe(statusCode); + expect(isHttpieError(error)).toBeTruthy(); + } + }); + + it("should decompress data when 'content-encoding' header is set with 'gzip'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "gzip" }; + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload.toString()); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should decompress data when 'content-encoding' header is set with 'x-gzip'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "x-gzip" }; + const payload = "Payload"; + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'br'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "br" }; + const payload = "Payload"; + const compressedPayload = await kAsyncBrotli(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'deflate'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "deflate" }; + const payload = "Payload"; + const compressedPayload = await kAsyncDeflate(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + }); + + describe("POST", () => { + it("should return a parsed response as text when 'content-type' header starts with 'text/'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = "La data."; + const buf = Buffer.from(payload); + + pool.intercept(target).reply(statusCode, buf, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a parsed response as object when 'content-type' header is set with 'application/json'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/json" }; + const payload = { my: "object" }; + const buf = Buffer.from(JSON.stringify(payload)); + + pool.intercept(target).reply(statusCode, buf, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a buffer when 'content-type' header is set with 'application/pdf'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/pdf" }; + const payload = Buffer.from("mon pdf"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(Buffer.from(payload)); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a buffer when 'content-type' header is set with unsupported value", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/msword" }; + const payload = Buffer.from(JSON.stringify({ my: "object" })); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should throw when 'content-type' header is set with unsupported value", async() => { + expect.assertions(5); + + const target = { + method: "POST", + path: "/test" + }; + const statusCode = 200; + const headers = { "content-type": "unknown" }; + const payload = Buffer.from(JSON.stringify({ my: "object" })); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path); + } + catch (error) { + expect(isHttpieError(error)).toBeTruthy(); + expect(error.message).toStrictEqual( + "An unexpected error occurred when trying to parse the response body (reason: 'invalid media type')." + ); + expect(error.buffer).toStrictEqual(payload); + expect(error.headers).toMatchObject(headers); + expect(error.statusCode).toBe(200); + } + }); + + it("should throw when 'content-encoding' header is set with unsupported value", async() => { + expect.assertions(5); + + const target = { + method: "POST", + path: "/test" + }; + + const payload = await kAsyncGzip("Mon document"); + const headers = { "content-encoding": "unknown" }; + const statusCode = 200; + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path); + } + catch (error) { + expect(error.message).toStrictEqual("Unsupported encoding 'unknown'."); + expect(error.buffer).toStrictEqual(payload); + expect(error.headers).toMatchObject(headers); + expect(error.statusCode).toBe(statusCode); + expect(isHttpieError(error)).toBeTruthy(); + } + }); + + it("should decompress data when 'content-encoding' header is set with 'gzip'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "gzip" }; + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload.toString()); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should decompress data when 'content-encoding' header is set with 'x-gzip'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "x-gzip" }; + const payload = "Payload"; + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'br'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "br" }; + const payload = "Payload"; + const compressedPayload = await kAsyncBrotli(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'deflate'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "deflate" }; + const payload = "Payload"; + const compressedPayload = await kAsyncDeflate(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + }); + + describe("PUT", () => { + it("should return a parsed response as text when 'content-type' header starts with 'text/'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = "La data."; + const buf = Buffer.from(payload); + + pool.intercept(target).reply(statusCode, buf, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a parsed response as object when 'content-type' header is set with 'application/json'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/json" }; + const payload = { my: "object" }; + const buf = Buffer.from(JSON.stringify(payload)); + + pool.intercept(target).reply(statusCode, buf, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a buffer when 'content-type' header is set with 'application/pdf'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/pdf" }; + const payload = Buffer.from("mon pdf"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(Buffer.from(payload)); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a buffer when 'content-type' header is set with unsupported value", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/msword" }; + const payload = Buffer.from(JSON.stringify({ my: "object" })); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should throw when 'content-type' header is set with unsupported value", async() => { + expect.assertions(5); + + const target = { + method: "PUT", + path: "/test" + }; + const statusCode = 200; + const headers = { "content-type": "unknown" }; + const payload = Buffer.from(JSON.stringify({ my: "object" })); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path); + } + catch (error) { + expect(isHttpieError(error)).toBeTruthy(); + expect(error.message).toStrictEqual( + "An unexpected error occurred when trying to parse the response body (reason: 'invalid media type')." + ); + expect(error.buffer).toStrictEqual(payload); + expect(error.headers).toMatchObject(headers); + expect(error.statusCode).toBe(200); + } + }); + + it("should throw when 'content-encoding' header is set with unsupported value", async() => { + expect.assertions(5); + + const target = { + method: "PUT", + path: "/test" + }; + + const payload = await kAsyncGzip("Mon document"); + const headers = { "content-encoding": "unknown" }; + const statusCode = 200; + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path); + } + catch (error) { + expect(error.message).toStrictEqual("Unsupported encoding 'unknown'."); + expect(error.buffer).toStrictEqual(payload); + expect(error.headers).toMatchObject(headers); + expect(error.statusCode).toBe(statusCode); + expect(isHttpieError(error)).toBeTruthy(); + } + }); + + it("should decompress data when 'content-encoding' header is set with 'gzip'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "gzip" }; + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload.toString()); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should decompress data when 'content-encoding' header is set with 'x-gzip'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "x-gzip" }; + const payload = "Payload"; + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'br'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "br" }; + const payload = "Payload"; + const compressedPayload = await kAsyncBrotli(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'deflate'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "deflate" }; + const payload = "Payload"; + const compressedPayload = await kAsyncDeflate(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + }); + + describe("DELETE", () => { + it("should return a parsed response as text when 'content-type' header starts with 'text/'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = "La data."; + const buf = Buffer.from(payload); + + pool.intercept(target).reply(statusCode, buf, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a parsed response as object when 'content-type' header is set with 'application/json'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/json" }; + const payload = { my: "object" }; + const buf = Buffer.from(JSON.stringify(payload)); + + pool.intercept(target).reply(statusCode, buf, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a buffer when 'content-type' header is set with 'application/pdf'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/pdf" }; + const payload = Buffer.from("mon pdf"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(Buffer.from(payload)); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a buffer when 'content-type' header is set with unsupported value", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/msword" }; + const payload = Buffer.from(JSON.stringify({ my: "object" })); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should throw when 'content-type' header is set with unsupported value", async() => { + expect.assertions(5); + + const target = { + method: "DELETE", + path: "/test" + }; + const statusCode = 200; + const headers = { "content-type": "unknown" }; + const payload = Buffer.from(JSON.stringify({ my: "object" })); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path); + } + catch (error) { + expect(isHttpieError(error)).toBeTruthy(); + expect(error.message).toStrictEqual( + "An unexpected error occurred when trying to parse the response body (reason: 'invalid media type')." + ); + expect(error.buffer).toStrictEqual(payload); + expect(error.headers).toMatchObject(headers); + expect(error.statusCode).toBe(200); + } + }); + + it("should throw when 'content-encoding' header is set with unsupported value", async() => { + expect.assertions(5); + + const target = { + method: "DELETE", + path: "/test" + }; + + const payload = await kAsyncGzip("Mon document"); + const headers = { "content-encoding": "unknown" }; + const statusCode = 200; + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path); + } + catch (error) { + expect(error.message).toStrictEqual("Unsupported encoding 'unknown'."); + expect(error.buffer).toStrictEqual(payload); + expect(error.headers).toMatchObject(headers); + expect(error.statusCode).toBe(statusCode); + expect(isHttpieError(error)).toBeTruthy(); + } + }); + + it("should decompress data when 'content-encoding' header is set with 'gzip'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "gzip" }; + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload.toString()); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should decompress data when 'content-encoding' header is set with 'x-gzip'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "x-gzip" }; + const payload = "Payload"; + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'br'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "br" }; + const payload = "Payload"; + const compressedPayload = await kAsyncBrotli(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'deflate'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "deflate" }; + const payload = "Payload"; + const compressedPayload = await kAsyncDeflate(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + }); + }); + + describe("DECOMPRESS mode", () => { + describe("GET", () => { + it("should return a buffer without parsing it even if 'content-type' header exists", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = Buffer.from("La data."); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should throw when 'content-encoding' header is set with unsupported value", async() => { + expect.assertions(5); + + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "unknown" }; + const payload = Buffer.from("Mon document"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + } + catch (error) { + expect(error.message).toStrictEqual("Unsupported encoding 'unknown'."); + expect(error.buffer).toMatchObject(payload); + expect(error.headers).toMatchObject(headers); + expect(isHttpieError(error)).toBeTruthy(); + expect(error.statusCode).toBe(200); + } + }); + + it("should throw when 'content-encoding' header is invalid", async() => { + expect.assertions(8); + + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "gzip" }; + const payload = await kAsyncBrotli("Mon document"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + } + catch (error) { + expect(error.reason).toBeTruthy(); + expect(error.message).toStrictEqual( + "An unexpected error occurred when trying to decompress the response body (reason: 'incorrect header check')." + ); + expect(error.buffer).toMatchObject(payload); + expect(error.headers).toMatchObject(headers); + expect(error.reason).toBeTruthy(); + expect(error.reason.message).toStrictEqual("incorrect header check"); + expect(isHttpieError(error)).toBeTruthy(); + expect(error.statusCode).toBe(200); + } + }); + + it("should decompress data when 'content-encoding' header is set with 'gzip'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "gzip" }; + const payload = Buffer.from("payload"); + const compressedPayload = await kAsyncGzip("payload"); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'x-gzip'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "x-gzip" }; + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'br'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncBrotli(payload); + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "br" }; + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'deflate'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncDeflate(payload); + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "deflate" }; + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + }); + + describe("POST", () => { + it("should return a buffer without parsing it even if 'content-type' header exists", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = Buffer.from("La data."); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should throw when 'content-encoding' header is set with unsupported value", async() => { + expect.assertions(5); + + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "unknown" }; + const payload = Buffer.from("Mon document"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + } + catch (error) { + expect(error.message).toStrictEqual("Unsupported encoding 'unknown'."); + expect(error.buffer).toMatchObject(payload); + expect(error.headers).toMatchObject(headers); + expect(isHttpieError(error)).toBeTruthy(); + expect(error.statusCode).toBe(200); + } + }); + + it("should throw when 'content-encoding' header is invalid", async() => { + expect.assertions(8); + + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "gzip" }; + const payload = await kAsyncBrotli("Mon document"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + } + catch (error) { + expect(error.reason).toBeTruthy(); + expect(error.message).toStrictEqual( + "An unexpected error occurred when trying to decompress the response body (reason: 'incorrect header check')." + ); + expect(error.buffer).toMatchObject(payload); + expect(error.headers).toMatchObject(headers); + expect(error.reason).toBeTruthy(); + expect(error.reason.message).toStrictEqual("incorrect header check"); + expect(isHttpieError(error)).toBeTruthy(); + expect(error.statusCode).toBe(200); + } + }); + + it("should decompress data when 'content-encoding' header is set with 'gzip'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "gzip" }; + const payload = Buffer.from("payload"); + const compressedPayload = await kAsyncGzip("payload"); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'x-gzip'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "x-gzip" }; + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'br'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncBrotli(payload); + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "br" }; + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'deflate'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncDeflate(payload); + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "deflate" }; + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + }); + + describe("PUT", () => { + it("should return a buffer without parsing it even if 'content-type' header exists", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = Buffer.from("La data."); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should throw when 'content-encoding' header is set with unsupported value", async() => { + expect.assertions(5); + + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "unknown" }; + const payload = Buffer.from("Mon document"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + } + catch (error) { + expect(error.message).toStrictEqual("Unsupported encoding 'unknown'."); + expect(error.buffer).toMatchObject(payload); + expect(error.headers).toMatchObject(headers); + expect(isHttpieError(error)).toBeTruthy(); + expect(error.statusCode).toBe(200); + } + }); + + it("should throw when 'content-encoding' header is invalid", async() => { + expect.assertions(8); + + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "gzip" }; + const payload = await kAsyncBrotli("Mon document"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + } + catch (error) { + expect(error.reason).toBeTruthy(); + expect(error.message).toStrictEqual( + "An unexpected error occurred when trying to decompress the response body (reason: 'incorrect header check')." + ); + expect(error.buffer).toMatchObject(payload); + expect(error.headers).toMatchObject(headers); + expect(error.reason).toBeTruthy(); + expect(error.reason.message).toStrictEqual("incorrect header check"); + expect(isHttpieError(error)).toBeTruthy(); + expect(error.statusCode).toBe(200); + } + }); + + it("should decompress data when 'content-encoding' header is set with 'gzip'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "gzip" }; + const payload = Buffer.from("payload"); + const compressedPayload = await kAsyncGzip("payload"); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'x-gzip'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "x-gzip" }; + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'br'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncBrotli(payload); + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "br" }; + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'deflate'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncDeflate(payload); + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "deflate" }; + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + }); + + describe("DELETE", () => { + it("should return a buffer without parsing it even if 'content-type' header exists", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = Buffer.from("La data."); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should throw when 'content-encoding' header is set with unsupported value", async() => { + expect.assertions(5); + + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "unknown" }; + const payload = Buffer.from("Mon document"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + } + catch (error) { + expect(error.message).toStrictEqual("Unsupported encoding 'unknown'."); + expect(error.buffer).toMatchObject(payload); + expect(error.headers).toMatchObject(headers); + expect(isHttpieError(error)).toBeTruthy(); + expect(error.statusCode).toBe(200); + } + }); + + it("should throw when 'content-encoding' header is invalid", async() => { + expect.assertions(8); + + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "gzip" }; + const payload = await kAsyncBrotli("Mon document"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + } + catch (error) { + expect(error.reason).toBeTruthy(); + expect(error.message).toStrictEqual( + "An unexpected error occurred when trying to decompress the response body (reason: 'incorrect header check')." + ); + expect(error.buffer).toMatchObject(payload); + expect(error.headers).toMatchObject(headers); + expect(error.reason).toBeTruthy(); + expect(error.reason.message).toStrictEqual("incorrect header check"); + expect(isHttpieError(error)).toBeTruthy(); + expect(error.statusCode).toBe(200); + } + }); + + it("should decompress data when 'content-encoding' header is set with 'gzip'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "gzip" }; + const payload = Buffer.from("payload"); + const compressedPayload = await kAsyncGzip("payload"); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'x-gzip'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "x-gzip" }; + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'br'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncBrotli(payload); + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "br" }; + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'deflate'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncDeflate(payload); + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "deflate" }; + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + const response = await request(target.method as any, kUrl + target.path, { mode: "decompress" }); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + }); + }); + + describe("RAW mode", () => { + describe("GET", () => { + it("should return a buffer without parsing it even if 'content-type' header exists", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = Buffer.from("payload"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { mode: "raw" }); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + + it("should return a buffer without decompress it even if 'content-encoding' header exists", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "gzip" }; + const payload = await kAsyncGzip("Doc"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + + const response = await request(target.method as any, kUrl + target.path, { mode: "raw" }); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + }); + + describe("POST", () => { + it("should return a buffer without parsing it even if 'content-type' header exists", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = Buffer.from("payload"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { mode: "raw" }); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + + it("should return a buffer without decompress it even if 'content-encoding' header exists", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "gzip" }; + const payload = await kAsyncGzip("Doc"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { mode: "raw" }); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + }); + + describe("PUT", () => { + it("should return a buffer without parsing it even if 'content-type' header exists", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = Buffer.from("payload"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { mode: "raw" }); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + + it("should return a buffer without decompress it even if 'content-encoding' header exists", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "gzip" }; + const payload = await kAsyncGzip("Doc"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + + const response = await request(target.method as any, kUrl + target.path, { mode: "raw" }); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + }); + + describe("DELETE", () => { + it("should return a buffer without parsing it even if 'content-type' header exists", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = Buffer.from("payload"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const response = await request(target.method as any, kUrl + target.path, { mode: "raw" }); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + + it("should return a buffer without decompress it even if 'content-encoding' header exists", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "gzip" }; + const payload = await kAsyncGzip("Doc"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + + const response = await request(target.method as any, kUrl + target.path, { mode: "raw" }); + + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + }); + }); +}); diff --git a/test/safeRequest.spec.ts b/test/safeRequest.spec.ts new file mode 100644 index 0000000..027bec2 --- /dev/null +++ b/test/safeRequest.spec.ts @@ -0,0 +1,2175 @@ +/* eslint-disable max-lines */ +// Import Third-Party Dependencies +import { Interceptable, MockAgent, setGlobalDispatcher } from "undici"; + +// Import Internal Dependencies +import { safeDel, safeGet, safePost, safePut } from "../src/request"; +import { brotliCompress, deflate, gzip } from "zlib"; +import { promisify } from "util"; +import { isHTTPError, isHttpieError } from "../src"; +import { randomInt } from "crypto"; + +// CONSTANTS +const kUrl = "http://test.com"; +const kAsyncGzip = promisify(gzip); +const kAsyncBrotli = promisify(brotliCompress); +const kAsyncDeflate = promisify(deflate); + +// VARS +let pool: Interceptable; + +describe("Httpie.safeRequest", () => { + beforeAll(() => { + const mockAgent = new MockAgent(); + setGlobalDispatcher(mockAgent); + mockAgent.disableNetConnect(); + + pool = mockAgent.get(kUrl); + }); + + describe("with ThrowOnHttpError", () => { + describe("GET", () => { + it("should throw if the response status code is higher than 400", async() => { + expect.assertions(5); + + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = randomInt(400, 503); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safeGet(kUrl + target.path); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(isHTTPError(error)).toBeTruthy(); + expect(error.statusCode).toBe(statusCode); + expect(error.data).toBe(payload.toString()); + expect(error.headers).toMatchObject(headers); + } + }); + + it("should not throw if the response status code is lower than 400", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = randomInt(200, 399); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safeGet(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + }); + + describe("POST", () => { + it("should throw if the response status code is higher than 400", async() => { + expect.assertions(5); + + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = randomInt(400, 503); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safePost(kUrl + target.path); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(isHTTPError(error)).toBeTruthy(); + expect(error.statusCode).toBe(statusCode); + expect(error.data).toBe(payload.toString()); + expect(error.headers).toMatchObject(headers); + } + }); + + it("should not throw if the response status code is lower than 400", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = randomInt(200, 399); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safePost(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + }); + + describe("PUT", () => { + it("should throw if the response status code is higher than 400", async() => { + expect.assertions(5); + + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = randomInt(400, 503); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safePut(kUrl + target.path); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(isHTTPError(error)).toBeTruthy(); + expect(error.statusCode).toBe(statusCode); + expect(error.data).toBe(payload.toString()); + expect(error.headers).toMatchObject(headers); + } + }); + + it("should not throw if the response status code is lower than 400", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = randomInt(200, 399); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safePut(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + }); + + describe("DELETE", () => { + it("should throw if the response status code is higher than 400", async() => { + expect.assertions(5); + + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = randomInt(400, 503); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safeDel(kUrl + target.path); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(isHTTPError(error)).toBeTruthy(); + expect(error.statusCode).toBe(statusCode); + expect(error.data).toBe(payload.toString()); + expect(error.headers).toMatchObject(headers); + } + }); + + it("should not throw if the response status code is lower than 400", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = randomInt(200, 399); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safeDel(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + }); + }); + + describe("without ThrowOnHttpError", () => { + describe("GET", () => { + it("should not throw if the response status code is higher than 400", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = randomInt(400, 503); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safeGet(kUrl + target.path, { throwOnHttpError: false }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + + it("should not throw if the response status code is lower than 400", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = randomInt(200, 399); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safeGet(kUrl + target.path, { throwOnHttpError: false }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + }); + + describe("POST", () => { + it("should not throw if the response status code is higher than 400", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = randomInt(400, 503); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safePost(kUrl + target.path, { throwOnHttpError: false }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + + it("should not throw if the response status code is lower than 400", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = randomInt(200, 399); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safePost(kUrl + target.path, { throwOnHttpError: false }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + }); + + describe("PUT", () => { + it("should not throw if the response status code is higher than 400", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = randomInt(400, 503); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safePut(kUrl + target.path, { throwOnHttpError: false }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + + it("should not throw if the response status code is lower than 400", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = randomInt(200, 399); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safePut(kUrl + target.path, { throwOnHttpError: false }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + }); + + describe("DELETE", () => { + it("should not throw if the response status code is higher than 400", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = randomInt(400, 503); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safeDel(kUrl + target.path, { throwOnHttpError: false }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + + it("should not throw if the response status code is lower than 400", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = randomInt(200, 399); + const headers = { "content-type": "text/html" }; + const payload = Buffer.from("Body"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safeDel(kUrl + target.path, { throwOnHttpError: false }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.statusCode).toBe(statusCode); + expect(response.data).toBe(payload.toString()); + expect(response.headers).toMatchObject(headers); + }); + }); + }); + + describe("PARSE mode (default)", () => { + describe("GET", () => { + it("should return a parsed response as text when 'content-type' header starts with 'text/'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = "La data."; + const buf = Buffer.from(payload); + + pool.intercept(target).reply(statusCode, buf, { headers }); + + const result = await safeGet(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a parsed response as object when 'content-type' header is set with 'application/json'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/json" }; + const payload = { my: "object" }; + const buf = Buffer.from(JSON.stringify(payload)); + + pool.intercept(target).reply(statusCode, buf, { headers }); + + const result = await safeGet(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a buffer when 'content-type' header is set with 'application/pdf'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/pdf" }; + const payload = Buffer.from("mon pdf"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safeGet(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(Buffer.from(payload)); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a buffer when 'content-type' header is set with unsupported value", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/msword" }; + const payload = Buffer.from(JSON.stringify({ my: "object" })); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safeGet(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should throw when 'content-type' header is set with unsupported value", async() => { + expect.assertions(6); + + const target = { + method: "GET", + path: "/test" + }; + const statusCode = 200; + const headers = { "content-type": "unknown" }; + const payload = Buffer.from(JSON.stringify({ my: "object" })); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safeGet(kUrl + target.path); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(isHttpieError(error)).toBeTruthy(); + expect(error.message).toStrictEqual( + "An unexpected error occurred when trying to parse the response body (reason: 'invalid media type')." + ); + expect(error.buffer).toStrictEqual(payload); + expect(error.headers).toMatchObject(headers); + expect(error.statusCode).toBe(200); + } + }); + + it("should throw when 'content-encoding' header is set with unsupported value", async() => { + expect.assertions(6); + + const target = { + method: "GET", + path: "/test" + }; + + const payload = await kAsyncGzip("Mon document"); + const headers = { "content-encoding": "unknown" }; + const statusCode = 200; + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safeGet(kUrl + target.path); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(error.message).toStrictEqual("Unsupported encoding 'unknown'."); + expect(error.buffer).toStrictEqual(payload); + expect(error.headers).toMatchObject(headers); + expect(error.statusCode).toBe(statusCode); + expect(isHttpieError(error)).toBeTruthy(); + } + }); + + it("should decompress data when 'content-encoding' header is set with 'gzip'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "gzip" }; + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safeGet(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload.toString()); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should decompress data when 'content-encoding' header is set with 'x-gzip'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "x-gzip" }; + const payload = "Payload"; + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safeGet(kUrl + target.path); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'br'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "br" }; + const payload = "Payload"; + const compressedPayload = await kAsyncBrotli(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safeGet(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'deflate'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "deflate" }; + const payload = "Payload"; + const compressedPayload = await kAsyncDeflate(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safeGet(kUrl + target.path); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + }); + + describe("POST", () => { + it("should return a parsed response as text when 'content-type' header starts with 'text/'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = "La data."; + const buf = Buffer.from(payload); + + pool.intercept(target).reply(statusCode, buf, { headers }); + + const result = await safePost(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a parsed response as object when 'content-type' header is set with 'application/json'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/json" }; + const payload = { my: "object" }; + const buf = Buffer.from(JSON.stringify(payload)); + + pool.intercept(target).reply(statusCode, buf, { headers }); + + const result = await safePost(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a buffer when 'content-type' header is set with 'application/pdf'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/pdf" }; + const payload = Buffer.from("mon pdf"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safePost(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(Buffer.from(payload)); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a buffer when 'content-type' header is set with unsupported value", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/msword" }; + const payload = Buffer.from(JSON.stringify({ my: "object" })); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safePost(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should throw when 'content-type' header is set with unsupported value", async() => { + expect.assertions(6); + + const target = { + method: "POST", + path: "/test" + }; + const statusCode = 200; + const headers = { "content-type": "unknown" }; + const payload = Buffer.from(JSON.stringify({ my: "object" })); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safePost(kUrl + target.path); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(isHttpieError(error)).toBeTruthy(); + expect(error.message).toStrictEqual( + "An unexpected error occurred when trying to parse the response body (reason: 'invalid media type')." + ); + expect(error.buffer).toStrictEqual(payload); + expect(error.headers).toMatchObject(headers); + expect(error.statusCode).toBe(200); + } + }); + + it("should throw when 'content-encoding' header is set with unsupported value", async() => { + expect.assertions(6); + + const target = { + method: "POST", + path: "/test" + }; + + const payload = await kAsyncGzip("Mon document"); + const headers = { "content-encoding": "unknown" }; + const statusCode = 200; + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safePost(kUrl + target.path); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(error.message).toStrictEqual("Unsupported encoding 'unknown'."); + expect(error.buffer).toStrictEqual(payload); + expect(error.headers).toMatchObject(headers); + expect(error.statusCode).toBe(statusCode); + expect(isHttpieError(error)).toBeTruthy(); + } + }); + + it("should decompress data when 'content-encoding' header is set with 'gzip'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "gzip" }; + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safePost(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload.toString()); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should decompress data when 'content-encoding' header is set with 'x-gzip'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "x-gzip" }; + const payload = "Payload"; + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safePost(kUrl + target.path); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'br'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "br" }; + const payload = "Payload"; + const compressedPayload = await kAsyncBrotli(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safePost(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'deflate'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "deflate" }; + const payload = "Payload"; + const compressedPayload = await kAsyncDeflate(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safePost(kUrl + target.path); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + }); + + describe("PUT", () => { + it("should return a parsed response as text when 'content-type' header starts with 'text/'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = "La data."; + const buf = Buffer.from(payload); + + pool.intercept(target).reply(statusCode, buf, { headers }); + + const result = await safePut(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a parsed response as object when 'content-type' header is set with 'application/json'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/json" }; + const payload = { my: "object" }; + const buf = Buffer.from(JSON.stringify(payload)); + + pool.intercept(target).reply(statusCode, buf, { headers }); + + const result = await safePut(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a buffer when 'content-type' header is set with 'application/pdf'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/pdf" }; + const payload = Buffer.from("mon pdf"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safePut(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(Buffer.from(payload)); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a buffer when 'content-type' header is set with unsupported value", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/msword" }; + const payload = Buffer.from(JSON.stringify({ my: "object" })); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safePut(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should throw when 'content-type' header is set with unsupported value", async() => { + expect.assertions(6); + + const target = { + method: "PUT", + path: "/test" + }; + const statusCode = 200; + const headers = { "content-type": "unknown" }; + const payload = Buffer.from(JSON.stringify({ my: "object" })); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safePut(kUrl + target.path); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(isHttpieError(error)).toBeTruthy(); + expect(error.message).toStrictEqual( + "An unexpected error occurred when trying to parse the response body (reason: 'invalid media type')." + ); + expect(error.buffer).toStrictEqual(payload); + expect(error.headers).toMatchObject(headers); + expect(error.statusCode).toBe(200); + } + }); + + it("should throw when 'content-encoding' header is set with unsupported value", async() => { + expect.assertions(6); + + const target = { + method: "PUT", + path: "/test" + }; + + const payload = await kAsyncGzip("Mon document"); + const headers = { "content-encoding": "unknown" }; + const statusCode = 200; + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safePut(kUrl + target.path); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(error.message).toStrictEqual("Unsupported encoding 'unknown'."); + expect(error.buffer).toStrictEqual(payload); + expect(error.headers).toMatchObject(headers); + expect(error.statusCode).toBe(statusCode); + expect(isHttpieError(error)).toBeTruthy(); + } + }); + + it("should decompress data when 'content-encoding' header is set with 'gzip'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "gzip" }; + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safePut(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload.toString()); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should decompress data when 'content-encoding' header is set with 'x-gzip'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "x-gzip" }; + const payload = "Payload"; + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safePut(kUrl + target.path); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'br'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "br" }; + const payload = "Payload"; + const compressedPayload = await kAsyncBrotli(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safePut(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'deflate'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "deflate" }; + const payload = "Payload"; + const compressedPayload = await kAsyncDeflate(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safePut(kUrl + target.path); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + }); + + describe("DELETE", () => { + it("should return a parsed response as text when 'content-type' header starts with 'text/'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = "La data."; + const buf = Buffer.from(payload); + + pool.intercept(target).reply(statusCode, buf, { headers }); + + const result = await safeDel(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a parsed response as object when 'content-type' header is set with 'application/json'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/json" }; + const payload = { my: "object" }; + const buf = Buffer.from(JSON.stringify(payload)); + + pool.intercept(target).reply(statusCode, buf, { headers }); + + const result = await safeDel(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a buffer when 'content-type' header is set with 'application/pdf'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/pdf" }; + const payload = Buffer.from("mon pdf"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safeDel(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(Buffer.from(payload)); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should return a buffer when 'content-type' header is set with unsupported value", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "application/msword" }; + const payload = Buffer.from(JSON.stringify({ my: "object" })); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safeDel(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should throw when 'content-type' header is set with unsupported value", async() => { + expect.assertions(6); + + const target = { + method: "DELETE", + path: "/test" + }; + const statusCode = 200; + const headers = { "content-type": "unknown" }; + const payload = Buffer.from(JSON.stringify({ my: "object" })); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safeDel(kUrl + target.path); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(isHttpieError(error)).toBeTruthy(); + expect(error.message).toStrictEqual( + "An unexpected error occurred when trying to parse the response body (reason: 'invalid media type')." + ); + expect(error.buffer).toStrictEqual(payload); + expect(error.headers).toMatchObject(headers); + expect(error.statusCode).toBe(200); + } + }); + + it("should throw when 'content-encoding' header is set with unsupported value", async() => { + expect.assertions(6); + + const target = { + method: "DELETE", + path: "/test" + }; + + const payload = await kAsyncGzip("Mon document"); + const headers = { "content-encoding": "unknown" }; + const statusCode = 200; + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safeDel(kUrl + target.path); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(error.message).toStrictEqual("Unsupported encoding 'unknown'."); + expect(error.buffer).toStrictEqual(payload); + expect(error.headers).toMatchObject(headers); + expect(error.statusCode).toBe(statusCode); + expect(isHttpieError(error)).toBeTruthy(); + } + }); + + it("should decompress data when 'content-encoding' header is set with 'gzip'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "gzip" }; + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safeDel(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload.toString()); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(statusCode); + }); + + it("should decompress data when 'content-encoding' header is set with 'x-gzip'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "x-gzip" }; + const payload = "Payload"; + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safeDel(kUrl + target.path); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'br'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "br" }; + const payload = "Payload"; + const compressedPayload = await kAsyncBrotli(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safeDel(kUrl + target.path); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'deflate'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "deflate" }; + const payload = "Payload"; + const compressedPayload = await kAsyncDeflate(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safeDel(kUrl + target.path); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + }); + }); + + describe("DECOMPRESS mode", () => { + describe("GET", () => { + it("should return a buffer without parsing it even if 'content-type' header exists", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = Buffer.from("La data."); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safeGet(kUrl + target.path, { mode: "decompress" }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should throw when 'content-encoding' header is set with unsupported value", async() => { + expect.assertions(6); + + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "unknown" }; + const payload = Buffer.from("Mon document"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safeGet(kUrl + target.path, { mode: "decompress" }); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(error.message).toStrictEqual("Unsupported encoding 'unknown'."); + expect(error.buffer).toMatchObject(payload); + expect(error.headers).toMatchObject(headers); + expect(isHttpieError(error)).toBeTruthy(); + expect(error.statusCode).toBe(200); + } + }); + + it("should throw when 'content-encoding' header is invalid", async() => { + expect.assertions(10); + + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "gzip" }; + const payload = await kAsyncBrotli("Mon document"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safeGet(kUrl + target.path, { mode: "decompress" }); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(err.cause).toBeTruthy(); + expect(error.reason).toBeTruthy(); + expect(error.message).toStrictEqual( + "An unexpected error occurred when trying to decompress the response body (reason: 'incorrect header check')." + ); + expect(error.buffer).toMatchObject(payload); + expect(error.headers).toMatchObject(headers); + expect(error.reason).toBeTruthy(); + expect(error.reason.message).toStrictEqual("incorrect header check"); + expect(isHttpieError(error)).toBeTruthy(); + expect(error.statusCode).toBe(200); + } + }); + + it("should decompress data when 'content-encoding' header is set with 'gzip'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "gzip" }; + const payload = Buffer.from("payload"); + const compressedPayload = await kAsyncGzip("payload"); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safeGet(kUrl + target.path, { mode: "decompress" }); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'x-gzip'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "x-gzip" }; + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safeGet(kUrl + target.path, { mode: "decompress" }); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'br'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncBrotli(payload); + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "br" }; + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + const result = await safeGet(kUrl + target.path, { mode: "decompress" }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'deflate'", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncDeflate(payload); + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "deflate" }; + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + const result = await safeGet(kUrl + target.path, { mode: "decompress" }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + }); + + describe("POST", () => { + it("should return a buffer without parsing it even if 'content-type' header exists", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = Buffer.from("La data."); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safePost(kUrl + target.path, { mode: "decompress" }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should throw when 'content-encoding' header is set with unsupported value", async() => { + expect.assertions(6); + + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "unknown" }; + const payload = Buffer.from("Mon document"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safePost(kUrl + target.path, { mode: "decompress" }); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(error.message).toStrictEqual("Unsupported encoding 'unknown'."); + expect(error.buffer).toMatchObject(payload); + expect(error.headers).toMatchObject(headers); + expect(isHttpieError(error)).toBeTruthy(); + expect(error.statusCode).toBe(200); + } + }); + + it("should throw when 'content-encoding' header is invalid", async() => { + expect.assertions(10); + + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "gzip" }; + const payload = await kAsyncBrotli("Mon document"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safePost(kUrl + target.path, { mode: "decompress" }); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(err.cause).toBeTruthy(); + expect(error.reason).toBeTruthy(); + expect(error.message).toStrictEqual( + "An unexpected error occurred when trying to decompress the response body (reason: 'incorrect header check')." + ); + expect(error.buffer).toMatchObject(payload); + expect(error.headers).toMatchObject(headers); + expect(error.reason).toBeTruthy(); + expect(error.reason.message).toStrictEqual("incorrect header check"); + expect(isHttpieError(error)).toBeTruthy(); + expect(error.statusCode).toBe(200); + } + }); + + it("should decompress data when 'content-encoding' header is set with 'gzip'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "gzip" }; + const payload = Buffer.from("payload"); + const compressedPayload = await kAsyncGzip("payload"); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safePost(kUrl + target.path, { mode: "decompress" }); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'x-gzip'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "x-gzip" }; + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safePost(kUrl + target.path, { mode: "decompress" }); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'br'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncBrotli(payload); + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "br" }; + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + const result = await safePost(kUrl + target.path, { mode: "decompress" }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'deflate'", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncDeflate(payload); + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "deflate" }; + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + const result = await safePost(kUrl + target.path, { mode: "decompress" }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + }); + + describe("PUT", () => { + it("should return a buffer without parsing it even if 'content-type' header exists", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = Buffer.from("La data."); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safePut(kUrl + target.path, { mode: "decompress" }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should throw when 'content-encoding' header is set with unsupported value", async() => { + expect.assertions(6); + + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "unknown" }; + const payload = Buffer.from("Mon document"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safePut(kUrl + target.path, { mode: "decompress" }); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(error.message).toStrictEqual("Unsupported encoding 'unknown'."); + expect(error.buffer).toMatchObject(payload); + expect(error.headers).toMatchObject(headers); + expect(isHttpieError(error)).toBeTruthy(); + expect(error.statusCode).toBe(200); + } + }); + + it("should throw when 'content-encoding' header is invalid", async() => { + expect.assertions(10); + + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "gzip" }; + const payload = await kAsyncBrotli("Mon document"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safePut(kUrl + target.path, { mode: "decompress" }); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(err.cause).toBeTruthy(); + expect(error.reason).toBeTruthy(); + expect(error.message).toStrictEqual( + "An unexpected error occurred when trying to decompress the response body (reason: 'incorrect header check')." + ); + expect(error.buffer).toMatchObject(payload); + expect(error.headers).toMatchObject(headers); + expect(error.reason).toBeTruthy(); + expect(error.reason.message).toStrictEqual("incorrect header check"); + expect(isHttpieError(error)).toBeTruthy(); + expect(error.statusCode).toBe(200); + } + }); + + it("should decompress data when 'content-encoding' header is set with 'gzip'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "gzip" }; + const payload = Buffer.from("payload"); + const compressedPayload = await kAsyncGzip("payload"); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safePut(kUrl + target.path, { mode: "decompress" }); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'x-gzip'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "x-gzip" }; + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safePut(kUrl + target.path, { mode: "decompress" }); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'br'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncBrotli(payload); + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "br" }; + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + const result = await safePut(kUrl + target.path, { mode: "decompress" }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'deflate'", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncDeflate(payload); + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "deflate" }; + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + const result = await safePut(kUrl + target.path, { mode: "decompress" }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + }); + + describe("DELETE", () => { + it("should return a buffer without parsing it even if 'content-type' header exists", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = Buffer.from("La data."); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safeDel(kUrl + target.path, { mode: "decompress" }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should throw when 'content-encoding' header is set with unsupported value", async() => { + expect.assertions(6); + + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "unknown" }; + const payload = Buffer.from("Mon document"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safeDel(kUrl + target.path, { mode: "decompress" }); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(error.message).toStrictEqual("Unsupported encoding 'unknown'."); + expect(error.buffer).toMatchObject(payload); + expect(error.headers).toMatchObject(headers); + expect(isHttpieError(error)).toBeTruthy(); + expect(error.statusCode).toBe(200); + } + }); + + it("should throw when 'content-encoding' header is invalid", async() => { + expect.assertions(10); + + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "gzip" }; + const payload = await kAsyncBrotli("Mon document"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + try { + const result = await safeDel(kUrl + target.path, { mode: "decompress" }); + expect(result.ok).toBeFalsy(); + result.unwrap(); + } + catch (err) { + const error = err.cause; + expect(err.cause).toBeTruthy(); + expect(error.reason).toBeTruthy(); + expect(error.message).toStrictEqual( + "An unexpected error occurred when trying to decompress the response body (reason: 'incorrect header check')." + ); + expect(error.buffer).toMatchObject(payload); + expect(error.headers).toMatchObject(headers); + expect(error.reason).toBeTruthy(); + expect(error.reason.message).toStrictEqual("incorrect header check"); + expect(isHttpieError(error)).toBeTruthy(); + expect(error.statusCode).toBe(200); + } + }); + + it("should decompress data when 'content-encoding' header is set with 'gzip'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "gzip" }; + const payload = Buffer.from("payload"); + const compressedPayload = await kAsyncGzip("payload"); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safeDel(kUrl + target.path, { mode: "decompress" }); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'x-gzip'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "x-gzip" }; + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncGzip(payload); + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + + const result = await safeDel(kUrl + target.path, { mode: "decompress" }); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'br'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncBrotli(payload); + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "br" }; + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + const result = await safeDel(kUrl + target.path, { mode: "decompress" }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + + it("should decompress data when 'content-encoding' header is set with 'deflate'", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const payload = Buffer.from("Payload"); + const compressedPayload = await kAsyncDeflate(payload); + + const statusCode = 200; + const headers = { "content-type": "text/html", "content-encoding": "deflate" }; + + pool.intercept(target).reply(statusCode, compressedPayload, { headers }); + const result = await safeDel(kUrl + target.path, { mode: "decompress" }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + }); + }); + }); + + describe("RAW mode", () => { + describe("GET", () => { + it("should return a buffer without parsing it even if 'content-type' header exists", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = Buffer.from("payload"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safeGet(kUrl + target.path, { mode: "raw" }); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + + it("should return a buffer without decompress it even if 'content-encoding' header exists", async() => { + const target = { + method: "GET", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "gzip" }; + const payload = await kAsyncGzip("Doc"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + + const result = await safeGet(kUrl + target.path, { mode: "raw" }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + }); + + describe("POST", () => { + it("should return a buffer without parsing it even if 'content-type' header exists", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = Buffer.from("payload"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safePost(kUrl + target.path, { mode: "raw" }); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + + it("should return a buffer without decompress it even if 'content-encoding' header exists", async() => { + const target = { + method: "POST", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "gzip" }; + const payload = await kAsyncGzip("Doc"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safePost(kUrl + target.path, { mode: "raw" }); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + }); + + describe("PUT", () => { + it("should return a buffer without parsing it even if 'content-type' header exists", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = Buffer.from("payload"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safePut(kUrl + target.path, { mode: "raw" }); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + + it("should return a buffer without decompress it even if 'content-encoding' header exists", async() => { + const target = { + method: "PUT", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "gzip" }; + const payload = await kAsyncGzip("Doc"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + + const result = await safePut(kUrl + target.path, { mode: "raw" }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + }); + + describe("DELETE", () => { + it("should return a buffer without parsing it even if 'content-type' header exists", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-type": "text/klsmdkf" }; + const payload = Buffer.from("payload"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + const result = await safeDel(kUrl + target.path, { mode: "raw" }); + expect(result.ok).toBeTruthy(); + + const response = result.unwrap(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + + it("should return a buffer without decompress it even if 'content-encoding' header exists", async() => { + const target = { + method: "DELETE", + path: "/test" + }; + + const statusCode = 200; + const headers = { "content-encoding": "gzip" }; + const payload = await kAsyncGzip("Doc"); + + pool.intercept(target).reply(statusCode, payload, { headers }); + + + const result = await safeDel(kUrl + target.path, { mode: "raw" }); + const response = result.unwrap(); + expect(result.ok).toBeTruthy(); + expect(response.data).toStrictEqual(payload); + expect(response.headers).toMatchObject(headers); + expect(response.statusCode).toBe(200); + }); + }); + }); +}); diff --git a/test/server/index.ts b/test/server/index.ts index 097cf90..d820aeb 100644 --- a/test/server/index.ts +++ b/test/server/index.ts @@ -80,6 +80,21 @@ export async function createServer(customPath = "local", port = 3000) { reply.send(); }); + server.get("/badEncoding", (request, reply) => { + reply.header("content-encoding", "oui"); + reply.send("{ 'foo': bar }"); + }); + + server.get("/pdf", (request, reply) => { + reply.header("content-type", "application/pdf"); + reply.send("{ 'foo': bar }"); + }); + + server.get("/text", (request, reply) => { + reply.header("content-type", "text/anything"); + reply.send("text"); + }); + await server.listen({ port }); return server; From 90abf847bf038d6e87c0d28df3871f1c66e6cd68 Mon Sep 17 00:00:00 2001 From: SofianD Date: Thu, 22 Feb 2024 17:14:37 +0100 Subject: [PATCH 5/9] update example of throwOnHttpError --- examples/throwOnHttpieError.mjs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/examples/throwOnHttpieError.mjs b/examples/throwOnHttpieError.mjs index 12e79fd..4c0cd4e 100644 --- a/examples/throwOnHttpieError.mjs +++ b/examples/throwOnHttpieError.mjs @@ -2,18 +2,20 @@ import * as httpie from "../dist/index.js"; // import * as httpie from "@myunisoft/httpie"; // Should not throw -async() => { - const { statusCode } = await httpie.request("GET", "127.0.0.1", { throwOnHttpError: false }); +{ + const { statusCode } = await httpie.request("GET", "127.0.0.1", { + throwOnHttpError: false + }); + console.log(statusCode) // 500 } // Should throw -async() => { - let statusCode; - try { - (statusCode = await httpie.request("GET", "127.0.0.1", { throwOnHttpError: true })); - } catch (error) { - console.log(statusCode) // undefined - console.log(error.statusCode) // 500 - } +try { + await httpie.request("GET", "127.0.0.1", { + throwOnHttpError: true + }); +} +catch (error) { + console.log(error.statusCode) // 500 } From c495dc3267d22da8edac8386d5b594153ba0a09c Mon Sep 17 00:00:00 2001 From: SofianD Date: Thu, 22 Feb 2024 17:28:18 +0100 Subject: [PATCH 6/9] update type of handler error --- src/class/HttpieHandlerError.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/class/HttpieHandlerError.ts b/src/class/HttpieHandlerError.ts index 81e6089..ed1253a 100644 --- a/src/class/HttpieHandlerError.ts +++ b/src/class/HttpieHandlerError.ts @@ -4,22 +4,24 @@ import { HttpieError, HttpieErrorOptions } from "./HttpieCommonError"; import { getDecompressionError, getFetchError, getParserError } from "../common/errors"; +type MessageOfGetDecompressionError = Parameters[0]["message"]; +type MessageOfGetParserError = Parameters[0]["message"]; +type MessageOfGetFetchError = Parameters[0]["message"]; + interface HttpieHandlerErrorOptions extends HttpieErrorOptions { /** @description original error */ error?: Error; message: T; } -// eslint-disable-next-line max-len -interface HttpieDecompressionErrorOptions extends HttpieHandlerErrorOptions[0]["message"]> { +interface HttpieDecompressionErrorOptions extends HttpieHandlerErrorOptions { /** @description original body as buffer */ buffer: Buffer; /** @description encodings from 'content-encoding' header */ encodings: string[]; } -// eslint-disable-next-line max-len -interface HttpieParserErrorOptions extends HttpieHandlerErrorOptions[0]["message"]> { +interface HttpieParserErrorOptions extends HttpieHandlerErrorOptions { /** @description content-type from 'content-type' header without params */ contentType: string; /** @description original body as buffer */ @@ -40,7 +42,7 @@ class HttpieHandlerError extends HttpieError { } export class HttpieFetchBodyError extends HttpieHandlerError { - constructor(options: HttpieHandlerErrorOptions[0]["message"]>, ...args) { + constructor(options: HttpieHandlerErrorOptions, ...args) { super(getFetchError(options, ...args), options); } } From e8795b54984eedafa82bc2c7e1affb81fba11bac Mon Sep 17 00:00:00 2001 From: SofianD Date: Thu, 22 Feb 2024 17:34:12 +0100 Subject: [PATCH 7/9] update doc --- docs/errors.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/errors.md b/docs/errors.md index dcc9d09..af68696 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -32,7 +32,7 @@ catch (error) { console.log(Boolean(error.statusCode)) // true } else { - // This error can be of any type of error. + // This error can be of any error type. console.log(Boolean(error.headers)) // false console.log(Boolean(error.statusCode)) // false } @@ -57,8 +57,11 @@ catch (error) { if (httpie.isHTTPError(error)) { console.log(Boolean(error.data)) // true console.log(Boolean(error.statusMessage)) // true + console.log(Boolean(error.headers)) // true + console.log(Boolean(error.statusCode)) // true } else { + // This error can be of any error type. console.log(Boolean(error.data)) // false console.log(Boolean(error.statusMessage)) // false } From e32cda5bdad48297279b71a2d968672add492713 Mon Sep 17 00:00:00 2001 From: SofianD Date: Thu, 22 Feb 2024 17:57:04 +0100 Subject: [PATCH 8/9] make Pierre happy again --- docs/errors.md | 5 +++-- examples/mode.ts | 10 +++++----- src/class/HttpieOnHttpError.ts | 2 +- src/utils.ts | 5 +++-- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/errors.md b/docs/errors.md index af68696..7bfcb8d 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -13,7 +13,7 @@ interface HttpieError { ### isHttpieError -The `isHttpieError` function can be used to find out if the error is an Httpie error. If the result is `false`, it is probably an error by Undici. +The `isHttpieError` function can be used to find out weither the error is a `@myunisoft/httpie` or a `undici` error. ```ts function isHttpieError(error: any): boolean; ``` @@ -74,7 +74,8 @@ catch (error) { If the `RequestOptions.throwOnHttpError` option is set to true, all HTTP responses with a status code higher than 400 will generate an `HttpieOnHttpError` error. -> [NOTE] Use [`isHTTPError`](#ishttperror) function to know if it is an HTTP error. +> [!NOTE] +> Use [`isHTTPError`](#ishttperror) function to know if it is an HTTP error. ```ts interface HttpieOnHttpError { diff --git a/examples/mode.ts b/examples/mode.ts index 1c3dd2a..b16bda5 100644 --- a/examples/mode.ts +++ b/examples/mode.ts @@ -1,17 +1,17 @@ import * as httpie from "../dist/index.js"; // import * as httpie from "@myunisoft/httpie"; -async() => { - const { data } = await httpie.request("GET", "127.0.0.1", { mode: "raw" }); +{ + const { data } = await httpie.request("GET", "127.0.0.1", { mode: "raw" }); console.log(data) // Buffer } -async() => { - const { data } = await httpie.request("GET", "127.0.0.1", { mode: "decompress" }); +{ + const { data } = await httpie.request("GET", "127.0.0.1", { mode: "decompress" }); console.log(data) // Buffer } -async() => { +{ const { data } = await httpie.request<{ key: "value" }>("GET", "127.0.0.1", { mode: "raw" }); console.log(data) // [Object] { key: "value" } } diff --git a/src/class/HttpieOnHttpError.ts b/src/class/HttpieOnHttpError.ts index 80307e4..6bccc61 100644 --- a/src/class/HttpieOnHttpError.ts +++ b/src/class/HttpieOnHttpError.ts @@ -4,7 +4,7 @@ import { RequestResponse } from "../request"; /** * @description Class to generate an Error with all the required properties from the response. - * We attach them to the error so that they can be retrieved by the developer in a Catch block. + * We attach these to the error so that they can be retrieved by the developer in a Catch block. */ export class HttpieOnHttpError> extends HttpieError { name = "HttpieOnHttpError"; diff --git a/src/utils.ts b/src/utils.ts index c427125..cb89f37 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -51,8 +51,9 @@ export function isHttpieError(error: unknown): error is HttpieError { return error instanceof HttpieError; } -// eslint-disable-next-line max-len -export function isHTTPError = RequestResponse>(error: unknown): error is HttpieOnHttpError { +export function isHTTPError = RequestResponse>( + error: unknown +): error is HttpieOnHttpError { return error instanceof HttpieOnHttpError; } From 74ef229435a9502a9a01607390c090aa16279dc6 Mon Sep 17 00:00:00 2001 From: SofianD Date: Thu, 22 Feb 2024 18:13:10 +0100 Subject: [PATCH 9/9] =?UTF-8?q?Stepping=20up=20the=20game=20=F0=9F=94=A5?= =?UTF-8?q?=20#noExcuses=20#RaisingTheBar=20=F0=9F=92=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/errors.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/errors.md b/docs/errors.md index 7bfcb8d..1e2ebb6 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -15,7 +15,7 @@ interface HttpieError { The `isHttpieError` function can be used to find out weither the error is a `@myunisoft/httpie` or a `undici` error. ```ts -function isHttpieError(error: any): boolean; +function isHttpieError(error: unknown): boolean; ``` Example: @@ -43,7 +43,7 @@ catch (error) { The `isHTTPError` function can be used to find out if it is an HTTP error. ```ts -function isHTTPError(error: any): boolean; +function isHTTPError(error: unknown): boolean; ``` Example: