Skip to content
This repository was archived by the owner on May 19, 2026. It is now read-only.
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
38 changes: 38 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

```bash
yarn build # Transpile src/ to dist/ with Babel
yarn test # Run Jest test suite
yarn test -t "name" # Run a single test by name pattern
yarn test --watch # Run tests in watch mode
yarn lint # Run ESLint + Prettier format check
yarn format # Auto-format src/ with Prettier
```

Docker-based dev environment (requires Docker + make):
```bash
make dev-setup dev-build # Initial setup
make dev-test # Run tests in Docker
make dev-test-watch # Watch-mode tests in Docker
```

## Architecture

`@hubble/request` is a lightweight, universal HTTP client wrapping the Fetch API. It targets both Node and browser environments via `isomorphic-unfetch`.

**Request flow:**
1. `src/request.js` — core function; encodes the request body (JSON-stringifies objects, converts GET body to query params), calls `fetch`, then parses the response based on `Content-Type`
2. On non-2xx status → throws `HttpError` (with body, statusCode, response, extra)
3. On network failure → throws `NetworkError` (with request, exception, extra)
4. On abort → throws `AbortError` (with request, exception)
5. On success → returns `ValidResponse` (with body, statusCode, response)

**Exports (`src/index.js`):** `request` (default), `AbortError`, `HttpError`, `NetworkError`, `ValidResponse`, `httpMethods`, `httpStatuses`

**Error distinction:** `HttpError` = server responded with 4xx/5xx. `NetworkError` = no response (DNS failure, timeout, etc.). `AbortError` = request cancelled via `request().abort()`.

**Aborting requests:** `request()` returns a promise with an `abort()` method. Calling it cancels the underlying fetch via an internal `AbortController` and rejects the promise with `AbortError`.
35 changes: 35 additions & 0 deletions src/AbortError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* An error resulting from a request being aborted via `request().abort()`.
* @memberof module:request
*/
class AbortError extends Error {
/**
* @param {Error} exception - the original AbortError thrown by fetch
* @param {object} request - the request url along with any options passed along
* @param {...any} args - further arguments to pass to `Error`
*/
constructor(exception, request, ...args) {
super(...args);

this.internalRequest = request;
this.internalException = exception;
}

/**
* Information about the aborted request, like the URL and headers
* @type {object}
*/
get request() {
return this.internalRequest;
}

/**
* The original exception that was thrown.
* @type {Error}
*/
get exception() {
return this.internalException;
}
}

export default AbortError;
48 changes: 48 additions & 0 deletions src/__tests__/request.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AbortError from '../AbortError';
import HttpError from '../HttpError';
import request from '../request';
import NetworkError from '../NetworkError';
Expand Down Expand Up @@ -443,4 +444,51 @@ describe('core : helpers : request : ', () => {
await expect(request(url)).rejects.toBeInstanceOf(NetworkError);
});
});

describe('abort : ', () => {
it('should expose an abort method on the returned promise', () => {
fetch.mockResponse('sample response - value is irrelevant');
const req = request(url);
expect(typeof req.abort).toBe('function');
});

it('should reject with an AbortError when aborted', async () => {
const abortError = new DOMException(
'The user aborted a request.',
'AbortError',
);
fetch.mockReject(abortError);

const req = request(url);
req.abort();

await expect(req).rejects.toBeInstanceOf(AbortError);
});

it('should include the exception on the AbortError', async () => {
const abortError = new DOMException(
'The user aborted a request.',
'AbortError',
);
fetch.mockReject(abortError);

const req = request(url);
req.abort();

await expect(req).rejects.toHaveProperty('exception', abortError);
});

it('should not throw a NetworkError when aborted', async () => {
const abortError = new DOMException(
'The user aborted a request.',
'AbortError',
);
fetch.mockReject(abortError);

const req = request(url);
req.abort();

await expect(req).rejects.not.toBeInstanceOf(NetworkError);
});
});
});
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default } from './request';
export { default as AbortError } from './AbortError';
export { default as HttpError } from './HttpError';
export { default as NetworkError } from './NetworkError';
export { default as method } from './httpMethods';
Expand Down
23 changes: 21 additions & 2 deletions src/request.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'isomorphic-unfetch';

import AbortError from './AbortError';
import HttpError from './HttpError';
import NetworkError from './NetworkError';
import ValidResponse from './ValidResponse';
Expand Down Expand Up @@ -137,10 +138,14 @@ const request = (
body = undefined,
opts = {},
) => {
const sendableOptions = getOptions(method, body, opts);
const controller = new AbortController();
const sendableOptions = getOptions(method, body, {
...opts,
signal: controller.signal,
});
const url = getUrl(urlArg, method, body);

return fetch(url, sendableOptions).then(
const promise = fetch(url, sendableOptions).then(
(response) => {
if (!response.ok) {
return createError(response).then((error) => Promise.reject(error));
Expand All @@ -149,6 +154,16 @@ const request = (
}
},
(error) => {
if (error.name === 'AbortError') {
return Promise.reject(
new AbortError(
error,
{ url, ...sendableOptions },
'The request was aborted.',
),
);
}

return Promise.reject(
new NetworkError(
error,
Expand All @@ -158,6 +173,10 @@ const request = (
);
},
);

promise.abort = () => controller.abort();

return promise;
};

export default request;
Loading