Skip to content

Commit

Permalink
Improve error handling and response parser (#232)
Browse files Browse the repository at this point in the history
* feat: add tool to decompress and parse.
  • Loading branch information
SofianD authored Feb 24, 2024
1 parent 2b2fe8d commit 42aa243
Show file tree
Hide file tree
Showing 22 changed files with 5,159 additions and 230 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 ✨

Expand Down
141 changes: 141 additions & 0 deletions docs/errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# 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 weither the error is a `@myunisoft/httpie` or a `undici` error.
```ts
function isHttpieError(error: unknown): 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 error type.
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: unknown): 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
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
}
}
```

---

## 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<T> {
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;
}
```
34 changes: 11 additions & 23 deletions docs/request.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
Expand Down Expand Up @@ -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<T>(response: RequestResponse<T>) {
const err = new Error(response.statusMessage) as Error & RequestResponse<T>;
err.statusMessage = response.statusMessage;
err.statusCode = response.statusCode;
err.headers = response.headers;
err.data = response.data;

return err;
}
```

17 changes: 17 additions & 0 deletions examples/mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as httpie from "../dist/index.js";
// import * as httpie from "@myunisoft/httpie";

{
const { data } = await httpie.request<Buffer>("GET", "127.0.0.1", { mode: "raw" });
console.log(data) // Buffer
}

{
const { data } = await httpie.request<Buffer>("GET", "127.0.0.1", { mode: "decompress" });
console.log(data) // Buffer
}

{
const { data } = await httpie.request<{ key: "value" }>("GET", "127.0.0.1", { mode: "raw" });
console.log(data) // [Object] { key: "value" }
}
21 changes: 21 additions & 0 deletions examples/throwOnHttpieError.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as httpie from "../dist/index.js";
// import * as httpie from "@myunisoft/httpie";

// Should not throw
{
const { statusCode } = await httpie.request("GET", "127.0.0.1", {
throwOnHttpError: false
});

console.log(statusCode) // 500
}

// Should throw
try {
await httpie.request("GET", "127.0.0.1", {
throwOnHttpError: true
});
}
catch (error) {
console.log(error.statusCode) // 500
}
23 changes: 23 additions & 0 deletions src/class/HttpieCommonError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Import Third-party Dependencies
import { IncomingHttpHeaders } from "undici/types/header";

type CommonResponseData = {
statusCode: number;
headers: IncomingHttpHeaders;
}

export interface HttpieErrorOptions {
response: CommonResponseData;
}

export class HttpieError extends Error {
headers: IncomingHttpHeaders;
statusCode: number;

constructor(message: string, options: HttpieErrorOptions) {
super(message);

this.statusCode = options.response.statusCode;
this.headers = options.response.headers;
}
}
74 changes: 74 additions & 0 deletions src/class/HttpieHandlerError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/* eslint-disable max-classes-per-file */

// Import Third-party Dependencies
import { HttpieError, HttpieErrorOptions } from "./HttpieCommonError";
import { getDecompressionError, getFetchError, getParserError } from "../common/errors";

type MessageOfGetDecompressionError = Parameters<typeof getDecompressionError>[0]["message"];
type MessageOfGetParserError = Parameters<typeof getParserError>[0]["message"];
type MessageOfGetFetchError = Parameters<typeof getFetchError>[0]["message"];

interface HttpieHandlerErrorOptions<T extends string = string> extends HttpieErrorOptions {
/** @description original error */
error?: Error;
message: T;
}

interface HttpieDecompressionErrorOptions extends HttpieHandlerErrorOptions<MessageOfGetDecompressionError> {
/** @description original body as buffer */
buffer: Buffer;
/** @description encodings from 'content-encoding' header */
encodings: string[];
}

interface HttpieParserErrorOptions extends HttpieHandlerErrorOptions<MessageOfGetParserError> {
/** @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: HttpieHandlerErrorOptions) {
super(message, options);

this.name = options.message;
this.reason = options.error ?? null;
}
}

export class HttpieFetchBodyError extends HttpieHandlerError {
constructor(options: HttpieHandlerErrorOptions<MessageOfGetFetchError>, ...args) {
super(getFetchError(options, ...args), options);
}
}

export class HttpieDecompressionError extends HttpieHandlerError {
buffer: Buffer;
encodings: string[];

constructor(options: HttpieDecompressionErrorOptions, ...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: HttpieParserErrorOptions, ...args) {
super(getParserError(options, ...args), options);

this.buffer = options.buffer;
this.contentType = options.contentType;
this.text = options.text ?? null;
}
}
21 changes: 21 additions & 0 deletions src/class/HttpieOnHttpError.ts
Original file line number Diff line number Diff line change
@@ -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 these to the error so that they can be retrieved by the developer in a Catch block.
*/
export class HttpieOnHttpError<T extends RequestResponse<any>> extends HttpieError {
name = "HttpieOnHttpError";

statusMessage: string;
data: T["data"];

constructor(response: T) {
super(response.statusMessage, { response });

this.statusMessage = response.statusMessage;
this.data = response.data;
}
}
Loading

0 comments on commit 42aa243

Please sign in to comment.