diff --git a/README.md b/README.md index e2e657c..8b4bf78 100644 --- a/README.md +++ b/README.md @@ -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: @@ -92,6 +93,23 @@ catch (error) { } ``` +Since v2.0.0 you can also use the `safe` prefix API to get a `Promise>` + +```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 diff --git a/package-lock.json b/package-lock.json index 432dac7..e8a3613 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.11.0", "license": "MIT", "dependencies": { + "@openally/result": "^1.2.0", "content-type": "^1.0.5", "lru-cache": "^10.0.0", "statuses": "^2.0.1", @@ -1207,6 +1208,14 @@ "eslint": "^8.32.0" } }, + "node_modules/@openally/result": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@openally/result/-/result-1.2.0.tgz", + "integrity": "sha512-BZvbLtpMmo1pp1pKi6XYQGWe+666uZP4vo4acR4U1Ezt2CvRlkg9ECfJac+2YJey8ggzh2g1vzHh1uOLUjh7zA==", + "engines": { + "node": ">=16.9.x" + } + }, "node_modules/@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", @@ -6558,6 +6567,11 @@ "eslint": "^8.32.0" } }, + "@openally/result": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@openally/result/-/result-1.2.0.tgz", + "integrity": "sha512-BZvbLtpMmo1pp1pKi6XYQGWe+666uZP4vo4acR4U1Ezt2CvRlkg9ECfJac+2YJey8ggzh2g1vzHh1uOLUjh7zA==" + }, "@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", diff --git a/package.json b/package.json index c04b38a..f678f00 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/request.ts b/src/request.ts index 0568cba..3a3cc12 100644 --- a/src/request.ts +++ b/src/request.ts @@ -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 @@ -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 = (fn: () => Promise) => Promise; -export interface ReqOptions { +export interface RequestError extends Error { + statusMessage: string; + statusCode: number; + headers: IncomingHttpHeaders; + data: E; +} + +export interface RequestOptions { /** Default: 0 */ maxRedirections?: number; /** Default: { "user-agent": "httpie" } */ @@ -46,7 +54,7 @@ export interface RequestResponse { export async function request( method: HttpMethod | WebDavMethod, uri: string | URL, - options: ReqOptions = {} + options: RequestOptions = {} ): Promise> { const { maxRedirections = 0 } = options; @@ -87,10 +95,31 @@ export async function request( return RequestResponse; } -export type RequestCallback = (uri: string | URL, options?: ReqOptions) => Promise>; +export async function safeRequest( + method: HttpMethod | WebDavMethod, + uri: string | URL, + options: RequestOptions = {} +): Promise, RequestError>> { + return Result.wrapAsync, RequestError>( + () => request(method, uri, options) + ); +} + +export type RequestCallback = ( + uri: string | URL, options?: RequestOptions +) => Promise>; +export type SafeRequestCallback = ( + uri: string | URL, options?: RequestOptions +) => Promise, RequestError>>; 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; diff --git a/src/stream.ts b/src/stream.ts index 6568cd3..103539f 100644 --- a/src/stream.ts +++ b/src/stream.ts @@ -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; +export type StreamOptions = Omit; export function pipeline( method: HttpMethod | WebDavMethod, diff --git a/src/utils.ts b/src/utils.ts index bcf09fa..5e1234f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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"; @@ -69,7 +69,7 @@ export async function parseUndiciResponse(response: Dispatcher.ResponseData): * - User-agent * - Authorization */ -export function createHeaders(options: Partial>): IncomingHttpHeaders { +export function createHeaders(options: Partial>): IncomingHttpHeaders { const headers = Object.assign(options.headers ?? {}, DEFAULT_HEADER); if (options.authorization) { headers.Authorization = createAuthorizationHeader(options.authorization); diff --git a/test/__snapshots__/request.spec.ts.snap b/test/__snapshots__/request.spec.ts.snap index 6fe7269..3979929 100644 --- a/test/__snapshots__/request.spec.ts.snap +++ b/test/__snapshots__/request.spec.ts.snap @@ -12,3 +12,16 @@ exports[`http.get should throw a 404 Not Found error because the path is not kno " `; + +exports[`http.safeGet should throw a 404 Not Found error because the path is not known 1`] = ` +" + +404 Not Found + +

Not Found

+

The requested URL was not found on this server.

+
+
Apache/2.4.54 (Debian) Server at ws-dev.myunisoft.fr Port 443
+ +" +`; diff --git a/test/request.spec.ts b/test/request.spec.ts index 15e1864..829b23c 100644 --- a/test/request.spec.ts +++ b/test/request.spec.ts @@ -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"; @@ -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("/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(); + } + }); +});