Skip to content

Commit 64c651d

Browse files
committed
feat: article improvements after review
1 parent 000423b commit 64c651d

1 file changed

Lines changed: 25 additions & 3 deletions

File tree

  • src/content/blog/nodejs-http-request

src/content/blog/nodejs-http-request/index.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ Making HTTP requests is one of the most common tasks in Node.js development. Whe
2323

2424
The good news is that modern Node.js includes everything you need to make HTTP requests without installing any external packages. If you've done HTTP requests in the browser before, what you'll learn today will feel very familiar. In this guide, we'll explore the built-in options and when to use each one.
2525

26+
:::note[Prerequisites]
27+
The examples in this guide use **top-level `await`**, which requires ESM (ECMAScript Modules). To use these examples, either:
28+
- Set `"type": "module"` in your `package.json`, or
29+
- Use the `.mjs` file extension
30+
31+
All examples assume Node.js 18 or later.
32+
:::
33+
2634
## Quick Answer: Use `fetch()`
2735

2836
I get it, you don't have time to become an expert on everything there's to know about making HTTP requests with Node.js, so here's the quick answer you might be looking for.
@@ -179,7 +187,7 @@ try {
179187
}
180188
```
181189

182-
`AbortSignal.timeout()` is available since Node.js 17.3 (or 16.14 LTS).
190+
`AbortSignal.timeout()` is available since Node.js 18 (or 17.3+).
183191

184192
### Handling Different Response Types
185193

@@ -380,6 +388,10 @@ This `fetchWithRetry` helper wraps `fetch()` and automatically retries failed re
380388

381389
Note that this example is opinionated: it always parses the response as JSON and treats any non-OK status code as an error worth retrying. For a more flexible approach, you could return the raw response and let the caller decide how to parse it, or add logic to only retry on specific status codes (like 429 Too Many Requests or 503 Service Unavailable).
382390

391+
:::caution[Idempotency Warning]
392+
This retry helper works best with **idempotent** GET requests. For POST/PUT/DELETE requests, retrying may cause duplicate side effects (like creating multiple orders). Additionally, if `options.body` is a stream, it can only be consumed once and retries will fail. For non-idempotent operations, either disable retries or implement request-specific retry logic.
393+
:::
394+
383395
### Form Data and File Uploads
384396

385397
The earlier [Streaming Uploads](#streaming-uploads) section shows how to stream a file as raw bytes, where the entire request body is just the file content. That approach works when the API accepts a raw binary body, but most real-world APIs expect the `multipart/form-data` format instead. This format follows web standards (it's the same encoding used by HTML forms with `enctype="multipart/form-data"`) and lets you include metadata fields like descriptions, tags, or user IDs alongside the file.
@@ -543,18 +555,26 @@ Here's how to test it with mocked responses:
543555
```javascript
544556
// user-service.test.js
545557
import assert from 'node:assert/strict'
546-
import { beforeEach, describe, it } from 'node:test'
547-
import { MockAgent, setGlobalDispatcher } from 'undici'
558+
import { afterEach, beforeEach, describe, it } from 'node:test'
559+
import { MockAgent, setGlobalDispatcher, getGlobalDispatcher } from 'undici'
548560
import { getUser } from './user-service.js'
549561

550562
describe('getUser', () => {
551563
let mockAgent
564+
let originalDispatcher
552565

553566
beforeEach(() => {
567+
originalDispatcher = getGlobalDispatcher()
554568
mockAgent = new MockAgent()
569+
mockAgent.disableNetConnect() // Prevent accidental real requests
555570
setGlobalDispatcher(mockAgent)
556571
})
557572

573+
afterEach(async () => {
574+
await mockAgent.close()
575+
setGlobalDispatcher(originalDispatcher) // Restore original dispatcher
576+
})
577+
558578
it('returns user data for valid id', async () => {
559579
const mockUser = { id: 1, name: 'Alice', email: 'alice@example.com' }
560580

@@ -582,6 +602,8 @@ describe('getUser', () => {
582602
})
583603
```
584604

605+
The `afterEach` cleanup is important: it closes the mock agent (releasing resources) and restores the original dispatcher. This prevents test pollution where mocks from one test leak into another, and avoids resource leaks if you're running many tests. The `disableNetConnect()` call adds an extra safety net by ensuring tests fail fast if they accidentally try to make real HTTP requests.
606+
585607
Let's break down what's happening in this test file. We import `describe`, `it`, and `beforeEach` from Node.js's built-in test runner (`node:test`), along with `assert` for assertions. In the `beforeEach` hook, we create a fresh `MockAgent` and register it as the global dispatcher using `setGlobalDispatcher()`. This tells undici (and therefore `fetch()`) to route all requests through our mock.
586608

587609
Each test then uses `mockAgent.get()` to target a specific origin, `.intercept()` to match a path and method, and `.reply()` to define the mocked response. When our `getUser()` function calls `fetch()`, it gets the mocked response instead of making a real network request.

0 commit comments

Comments
 (0)