Skip to content
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
100 changes: 53 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,100 +27,106 @@

## 📢 About

The package is inspired by lukeed [httpie](https://github.com/lukeed/httpie) (The use is relatively similar). This package use new Node.js http client [undici](https://github.com/nodejs/undici) under the hood.
Httpie is a Node.js HTTP client built on [Undici](https://github.com/nodejs/undici). Its request API follows the small, function-based style of lukeed's [httpie](https://github.com/lukeed/httpie), with response parsing, agent selection, rate limiting, and `Result`-based error handling added around it.

## 🔬 Features

- 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.
- Safe error handling with Rust-like [Result](https://github.com/OpenAlly/npm-packages/tree/main/src/result).

Thanks to undici:

- Support [redirections](https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections) or retry using interceptors.
- Implement high-level API for undici **stream** and **pipeline** method.
- High performance (see [benchmarks](https://undici.nodejs.org/#/?id=benchmarks)).
- Work well with **newest** Node.js API [AbortController](https://nodejs.org/dist/latest-v16.x/docs/api/globals.html#globals_class_abortcontroller) to cancel http request.

Light with seriously maintained dependencies:

![](./docs/images/nodesecure.PNG)
- Parses JSON and text responses from their `content-type`; other response bodies remain buffers.
- Decompresses encoded responses before parsing them.
- Provides `get`, `post`, `put`, `patch`, and `del` aliases alongside the general `request` function.
- Provides `safeRequest` and safe HTTP verb aliases that return a [Result](https://github.com/OpenAlly/npm-packages/tree/main/src/result).
- Selects an Undici dispatcher from a registered origin or path, with cached URI resolution.
- Accepts a rate-limiter callback through the `limit` option or an agent registration.

## 🚧 Requirements

- [Node.js](https://nodejs.org/en/) version 22 or higher

## 🚀 Getting Started

This package is available in the Node Package Repository and can be easily installed with [npm](https://docs.npmjs.com/getting-started/what-is-npm) or [yarn](https://yarnpkg.com).
Install the package with [npm](https://docs.npmjs.com/getting-started/what-is-npm) or [yarn](https://yarnpkg.com):

```bash
$ npm i @openally/httpie
npm install @openally/httpie
# or
$ yarn add @openally/httpie
yarn add @openally/httpie
```

## 📚 Usage example

This client is very similar to lukeed httpie http client.

```js
import * as httpie from "@openally/httpie";
```ts
import {
get,
post,
isHTTPError
} from "@openally/httpie";

interface Post {
id: number;
title: string;
body: string;
userId: number;
}

try {
const { data } = await httpie.get("https://jsonplaceholder.typicode.com/posts");
console.log(data);

const response = await httpie.post("https://jsonplaceholder.typicode.com/posts", {
const { data: posts } = await get<Post[]>(
"https://jsonplaceholder.typicode.com/posts"
);
console.log(posts);

const response = await post<Post>("https://jsonplaceholder.typicode.com/posts", {
body: {
title: "foo",
body: "bar",
userId: 1
}
});

console.log(response.statusCode);
console.log(response.statusMessage);
console.log(response.data);
console.log(response.statusCode, response.data);
}
catch (error) {
console.log(error.message);
console.log(error.statusCode);
console.log(error.headers);
console.log(error.data);
catch (error: unknown) {
if (isHTTPError(error)) {
console.error(error.statusCode, error.data);
}
else {
throw error;
}
}
```

You can also use the `safe` prefix API to get a `Promise<Result<T, E>>`
The `safe` methods return a `Result` instead of throwing:

```ts
import * as httpie from "@openally/httpie";
import { safePost } from "@openally/httpie";

const response = (await httpie.safePost("https://jsonplaceholder.typicode.com/posts", {
const result = await 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();
});

result.match(
(response) => console.log(response.data),
(error) => console.error(error.message)
);
```

> [!TIP]
> More examples available in the root folder **examples**.

## 📜 API

- [Request API](./docs/request.md)
- [Work and manage Agents](./docs/agents.md)
- [Requests, options, response modes, and safe methods](./docs/request.md)
- [Streams and pipelines](./docs/stream.md)
- [Agent registry and URI resolution](./docs/agents.md)

Httpie also re-exports selected Undici APIs. Their behavior follows the [Undici documentation](https://undici.nodejs.org).

## Error handling

Read the [error documentation](./docs/errors.md).
Read [Error handling](./docs/errors.md) for thrown errors, safe results, and the `isHttpieError` and `isHTTPError` guards.

## Contributors ✨

Expand Down
134 changes: 116 additions & 18 deletions docs/agents.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,132 @@
# Agents

Agents are custom constructs that are used to describe internal and external services.
The `agents` registry maps a service path or hostname to an Undici dispatcher. Register an agent once, then use either a short service path such as `/catalog/items` or the service's absolute URL in `request()`, its HTTP verb aliases, `stream()`, and `pipeline()`.

```js
import { agents } from "@openally/httpie";

console.log(agents); // <- push a new agent in this Array
```

Those custom `agents` are described by the following TypeScript interface:
```ts
export interface CustomHttpAgent {
interface CustomHttpAgent {
customPath: string;
origin: string;
agent: Agent;
agent: Agent | ProxyAgent | MockAgent;
limit?: InlineCallbackAction;
}

const agents: Set<CustomHttpAgent>;
```

Example with a test custom agent:
## Register an agent

```ts
export const test: CustomHttpAgent = {
customPath: "test",
import {
Agent,
agents,
get,
type CustomHttpAgent
} from "@openally/httpie";

const catalog: CustomHttpAgent = {
customPath: "catalog",
origin: "https://catalog.example.com",
agent: new Agent({
connections: 30
}),
origin: "https://test.domain.fr"
})
};

agents.add(catalog);

const { data } = await get("/catalog/items");
```

The request path `/catalog/items` becomes `https://catalog.example.com/items` and uses `catalog.agent` as its Undici dispatcher.

`agents` is a standard JavaScript `Set`. Keep a reference to an entry if it may need to be removed later:

```ts
agents.delete(catalog);
```

## Matching rules

A string URI is resolved in this order:

1. Httpie checks each registered `customPath` in insertion order. Both `/catalog/items` and `catalog/items` match `customPath: "catalog"`.
2. If no path matches, Httpie parses the string as an absolute URL and looks for an agent whose `origin` has the same hostname.

A WHATWG `URL` is matched by hostname. Path aliases apply only to string URIs.

The first matching entry wins. Use distinct path prefixes when several agents are registered.

```ts
const internalApi: CustomHttpAgent = {
customPath: "internal",
origin: "https://api.example.com",
agent: new Agent()
};

// Note: push it to the package agents list
agents.add(test);
agents.add(internalApi);

await get("/internal/users");
await get("https://api.example.com/users");
```

The **agent** property is an Undici Agent.
Both requests use `internalApi.agent`.

## Per-request overrides

The `agent` option on a request takes precedence over the dispatcher selected from the registry:

```ts
import {
ProxyAgent,
get
} from "@openally/httpie";

const proxy = new ProxyAgent("http://proxy.example.com:8080");

const response = await get("/catalog/items", {
agent: proxy
});
```

An agent entry may also provide a `limit` callback. A request-level `limit` overrides the registered one.

```ts
const catalog: CustomHttpAgent = {
customPath: "catalog",
origin: "https://catalog.example.com",
agent: new Agent(),
limit: async(callback) => callback()
};
```

See [Request options](./request.md#request-options) for the callback signature.

## `computeURI()`

`computeURI()` exposes the resolution used internally by requests and streams.

```ts
computeURI(
uri: string | URL
): {
url: URL;
agent: Agent | ProxyAgent | MockAgent | null;
limit?: InlineCallbackAction;
}
```

```ts
import { computeURI } from "@openally/httpie";

const resolved = computeURI("/catalog/items");

console.log(resolved.url.href);
// https://catalog.example.com/items
```

URI resolutions are cached by input string. The cache holds up to 100 entries for 120 minutes. Register agents before sending requests or calling `computeURI()`, because changing the `agents` set does not invalidate entries already in the cache.

The returned `URL` can be changed by the caller without modifying the cached URL.

## Undici dispatchers

Httpie re-exports the dispatcher classes used by the registry, including `Agent`, `ProxyAgent`, and `MockAgent`. It also re-exports `Client`, interceptors, global dispatcher helpers, and Undici's mocking utilities. Configuration and lifecycle behavior for those exports follows the [Undici documentation](https://undici.nodejs.org).
Loading
Loading