Skip to content

Commit 9810110

Browse files
authored
feat: initial implementation (#2)
Signed-off-by: Miroslav Bajtoš <[email protected]>
1 parent 86b30fa commit 9810110

File tree

4 files changed

+132
-21
lines changed

4 files changed

+132
-21
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
* @bajtos
1+
* @bajtos @juliangruber @pyropy @NikolasHaimerl
22
*/package.json
33
package-lock.json

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,37 @@
11
# assert-ok-response
22

33
A tiny helper to assert that response from `fetch()` is ok.
4+
5+
## Usage
6+
7+
```js
8+
import { assertOkResponse } from 'assert-ok-response'
9+
10+
const response = await fetch('https://example.com/not-found')
11+
await assertOkResponse(response)
12+
//
13+
// Throws:
14+
//
15+
// const err = new Error(`${errorMsg ?? `Cannot fetch ${res.url}`} (${res.status}): ${body}`)
16+
// ^
17+
//
18+
// Error: Cannot fetch https://example.com/not-found (404): <!doctype html>
19+
// [...]
20+
// at assertOkResponse (file:///Users/bajtos/src/assert-ok-response/index.js:15:15)
21+
// at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
22+
// at async file:///Users/bajtos/src/assert-ok-response/x.js:4:1 {
23+
// statusCode: 404,
24+
// serverMessage: '<!doctype html>\n' +
25+
// [...]
26+
```
27+
28+
Error properties:
29+
30+
- `statusCode` (number) - The HTTP status code of the response.
31+
- `serverMessage` (string) - The response body returned by the server.
32+
33+
## Development
34+
35+
- `npm test` to run the tests.
36+
- `npm run lint:fix` to fix linting issues.
37+
- `npx np` to publish a new version.

index.js

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
/**
2-
* @param {string | URL} url
3-
* @param {object} options
4-
* @param {string[][] | Record<string, string | ReadonlyArray<string>> | Headers} [options.requestHeaders]
5-
* @param {typeof fetch} [options.fetchFn] Custom fetch function (useful for testing)
6-
* @param {console['log']} [options.debugLog] Debug logger
2+
* @param {Response} res The Response object returned by a fetch request.
3+
* @param {string} [errorMsg] An optional error message to include in the error thrown if the response is not ok.
74
*/
8-
export function createJsonRpcClient(url, { requestHeaders, fetchFn } = {}) {
9-
fetchFn = fetchFn ?? fetch
5+
export async function assertOkResponse(res, errorMsg) {
6+
if (res.ok) return
107

11-
/**
12-
* @param {string} method
13-
* @param {[unknown]} [params]
14-
*/
15-
const rpc = async (method, params) => {
16-
// TODO
8+
let body
9+
try {
10+
body = await res.text()
11+
} catch (/** @type {any} */ err) {
12+
body = `(Cannot read response body: ${err.message ?? err})`
1713
}
18-
return rpc
14+
body = body?.trimEnd()
15+
const err = new Error(`${errorMsg ?? `Cannot fetch ${res.url}`} (${res.status}): ${body}`)
16+
Object.assign(err, {
17+
statusCode: res.status,
18+
serverMessage: body,
19+
})
20+
throw err
1921
}

test.js

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,85 @@
11
import { describe, it } from 'node:test'
2-
import assert from 'node:assert'
3-
import { createJsonRpcClient } from './index.js'
2+
import assert, { AssertionError } from 'node:assert'
3+
import { assertOkResponse } from './index.js'
44

5-
describe('assert-ok-response', () => {
6-
it('should create a JSON-RPC client', () => {
7-
const client = createJsonRpcClient('http://example.com')
8-
assert.ok(client)
5+
describe('assertOkResponse', () => {
6+
it('accepts ok (200)', async () => {
7+
const response = createFakeResponse({
8+
ok: true,
9+
})
10+
11+
await assertOkResponse(response)
12+
})
13+
14+
it('rejects !ok (404)', async () => {
15+
const response = createFakeResponse({
16+
ok: false,
17+
status: 404,
18+
async text() {
19+
return 'endpoint not found'
20+
},
21+
})
22+
23+
const error = await assertRejects(() => assertOkResponse(response))
24+
assert.equal(error.message, 'Cannot fetch / (404): endpoint not found')
25+
assert.equal(error.statusCode, 404)
26+
assert.equal(error.serverMessage, 'endpoint not found')
27+
})
28+
29+
it('uses custom message when provided', async () => {
30+
const response = createFakeResponse({ ok: false })
31+
const error = await assertRejects(() =>
32+
assertOkResponse(response, 'Cannot post the measurement'),
33+
)
34+
assert.match(error.message, /^Cannot post the measurement/)
35+
})
36+
37+
it('handles error while reading error response body', async () => {
38+
const response = createFakeResponse({
39+
ok: false,
40+
status: 500,
41+
async text() {
42+
throw new Error('chunked encoding error')
43+
},
44+
})
45+
46+
const error = await assertRejects(() => assertOkResponse(response))
47+
assert.equal(
48+
error.message,
49+
'Cannot fetch / (500): (Cannot read response body: chunked encoding error)',
50+
)
51+
assert.equal(error.statusCode, 500)
52+
assert.equal(error.serverMessage, '(Cannot read response body: chunked encoding error)')
953
})
1054
})
55+
56+
// Note: the built-in assert.rejects() does not return the rejection (the error)
57+
async function assertRejects(fn) {
58+
try {
59+
await fn()
60+
} catch (err) {
61+
return err
62+
}
63+
throw new AssertionError('Expected promise to reject')
64+
}
65+
66+
/**
67+
* @param {Partial<Response>} props
68+
* @returns {Response}
69+
*/
70+
function createFakeResponse(props = {}) {
71+
const status = props.status ?? (props.ok ? 200 : 500)
72+
const ok = props.ok ?? props.status < 400
73+
return {
74+
headers: new Headers(),
75+
url: '/',
76+
async text() {
77+
return '(empty response body)'
78+
},
79+
80+
...props,
81+
82+
ok,
83+
status,
84+
}
85+
}

0 commit comments

Comments
 (0)