Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve error handling and response parser #232

Merged
merged 9 commits into from
Feb 24, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
137 changes: 137 additions & 0 deletions docs/errors.md
Original file line number Diff line number Diff line change
@@ -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.
SofianD marked this conversation as resolved.
Show resolved Hide resolved
```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.
SofianD marked this conversation as resolved.
Show resolved Hide resolved

```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";
fraxken marked this conversation as resolved.
Show resolved Hide resolved

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" }
}
SofianD marked this conversation as resolved.
Show resolved Hide resolved
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";
fraxken marked this conversation as resolved.
Show resolved Hide resolved

// 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 them to the error so that they can be retrieved by the developer in a Catch block.
SofianD marked this conversation as resolved.
Show resolved Hide resolved
*/
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
Loading