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

feat!: introduce safe method returning Result #94

Merged
merged 1 commit into from
Jul 8, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ The package is inspired by lukeed [httpie](https://github.com/lukeed/httpie) (Th
- 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.
- Built-in retry mechanism with **custom policies**.
- Safe error handling with Rust-like [Result](https://github.com/OpenAlly/npm-packages/tree/main/src/result).

Thanks to undici:

Expand Down Expand Up @@ -92,6 +93,23 @@ catch (error) {
}
```

Since v2.0.0 you can also use the `safe` prefix API to get a `Promise<Result<T, E>>`

```ts
import * as httpie from "@myunisoft/httpie";

const response = (await httpie.safePost("https://jsonplaceholder.typicode.com/posts", {
body: {
title: "foo",
body: "bar",
userId: 1
}
}))
.map((response) => response.data)
.mapErr((error) => new Error("a message here!", { cause: error.data }));
.unwrap();
```

> 👀 For more examples of use please look at the root folder **examples**.

## 📜 API
Expand Down
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"typescript": "^4.9.5"
},
"dependencies": {
"@openally/result": "^1.2.0",
"content-type": "^1.0.5",
"lru-cache": "^10.0.0",
"statuses": "^2.0.1",
Expand Down
35 changes: 32 additions & 3 deletions src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { URLSearchParams } from "url";

// Import Third-party Dependencies
import * as undici from "undici";
import { Result } from "@openally/result";
import status from "statuses";

// Import Internal Dependencies
Expand All @@ -14,7 +15,14 @@ export type WebDavMethod = "MKCOL" | "COPY" | "MOVE" | "LOCK" | "UNLOCK" | "PROP
export type HttpMethod = "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE" | "PATCH" ;
export type InlineCallbackAction = <T>(fn: () => Promise<T>) => Promise<T>;

export interface ReqOptions {
export interface RequestError<E> extends Error {
statusMessage: string;
statusCode: number;
headers: IncomingHttpHeaders;
data: E;
}

export interface RequestOptions {
/** Default: 0 */
maxRedirections?: number;
/** Default: { "user-agent": "httpie" } */
Expand Down Expand Up @@ -46,7 +54,7 @@ export interface RequestResponse<T> {
export async function request<T>(
method: HttpMethod | WebDavMethod,
uri: string | URL,
options: ReqOptions = {}
options: RequestOptions = {}
): Promise<RequestResponse<T>> {
const { maxRedirections = 0 } = options;

Expand Down Expand Up @@ -87,10 +95,31 @@ export async function request<T>(
return RequestResponse;
}

export type RequestCallback = <T>(uri: string | URL, options?: ReqOptions) => Promise<RequestResponse<T>>;
export async function safeRequest<T, E>(
method: HttpMethod | WebDavMethod,
uri: string | URL,
options: RequestOptions = {}
): Promise<Result<RequestResponse<T>, RequestError<E>>> {
return Result.wrapAsync<RequestResponse<T>, RequestError<E>>(
() => request(method, uri, options)
);
}

export type RequestCallback = <T>(
uri: string | URL, options?: RequestOptions
) => Promise<RequestResponse<T>>;
export type SafeRequestCallback = <T, E>(
uri: string | URL, options?: RequestOptions
) => Promise<Result<RequestResponse<T>, RequestError<E>>>;

export const get = request.bind(null, "GET") as RequestCallback;
export const post = request.bind(null, "POST") as RequestCallback;
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;

export const safeGet = safeRequest.bind(null, "GET") as SafeRequestCallback;
export const safePost = safeRequest.bind(null, "POST") as SafeRequestCallback;
export const safePut = safeRequest.bind(null, "PUT") as SafeRequestCallback;
export const safeDel = safeRequest.bind(null, "DELETE") as SafeRequestCallback;
export const safePatch = safeRequest.bind(null, "PATCH") as SafeRequestCallback;
4 changes: 2 additions & 2 deletions src/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { Duplex, Writable } from "stream";
import * as undici from "undici";

// Import Internal Dependencies
import { ReqOptions, HttpMethod, WebDavMethod } from "./request";
import { RequestOptions, HttpMethod, WebDavMethod } from "./request";
import { computeURI } from "./agents";
import * as Utils from "./utils";

export type StreamOptions = Omit<ReqOptions, "limit">;
export type StreamOptions = Omit<RequestOptions, "limit">;

export function pipeline(
method: HttpMethod | WebDavMethod,
Expand Down
4 changes: 2 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as contentType from "content-type";
import { Dispatcher } from "undici";

// Import Internal Dependencies
import { RequestResponse, ReqOptions } from "./request";
import { RequestResponse, RequestOptions } from "./request";

// CONSTANTS
const kDefaultMimeType = "text/plain";
Expand Down Expand Up @@ -69,7 +69,7 @@ export async function parseUndiciResponse<T>(response: Dispatcher.ResponseData):
* - User-agent
* - Authorization
*/
export function createHeaders(options: Partial<Pick<ReqOptions, "headers" | "authorization">>): IncomingHttpHeaders {
export function createHeaders(options: Partial<Pick<RequestOptions, "headers" | "authorization">>): IncomingHttpHeaders {
const headers = Object.assign(options.headers ?? {}, DEFAULT_HEADER);
if (options.authorization) {
headers.Authorization = createAuthorizationHeader(options.authorization);
Expand Down
13 changes: 13 additions & 0 deletions test/__snapshots__/request.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,16 @@ exports[`http.get should throw a 404 Not Found error because the path is not kno
</body></html>
"
`;

exports[`http.safeGet should throw a 404 Not Found error because the path is not known 1`] = `
"<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.</p>
<hr>
<address>Apache/2.4.54 (Debian) Server at ws-dev.myunisoft.fr Port 443</address>
</body></html>
"
`;
27 changes: 26 additions & 1 deletion test/request.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { FastifyInstance } from "fastify";

// Import Internal Dependencies
import { get, post, put, patch, del } from "../src/index";
import { get, post, put, patch, del, safeGet } from "../src/index";

// Helpers and mock
import { createServer } from "./server/index";
Expand Down Expand Up @@ -161,3 +161,28 @@ describe("http.del", () => {
expect(statusCode).toStrictEqual(200);
});
});

describe("http.safeGet", () => {
it("should GET uptime from local fastify server", async() => {
const result = await safeGet<{ uptime: number }, any>("/local/");

expect(result.ok).toStrictEqual(true);
const { data } = result.unwrap();
expect("uptime" in data).toStrictEqual(true);
expect(typeof data.uptime).toStrictEqual("number");
});

it("should throw a 404 Not Found error because the path is not known", async() => {
const result = await safeGet<string, any>("/windev/hlkezcjcke");
expect(result.err).toStrictEqual(true);

if (result.err) {
const error = result.val;

expect(error.name).toStrictEqual("Error");
expect(error.statusCode).toStrictEqual(404);
expect(error.statusMessage).toStrictEqual("Not Found");
expect(error.data).toMatchSnapshot();
}
});
});