jsrosetta

I/O

HTTP Client

fetch() toàn cục của Node.js so với net/http của Go: GET và đọc JSON, POST kèm header, mã lỗi 4xx/5xx, timeout và đọc body dạng stream.

Tác giả:
Phiên bản tối thiểu
Node.js ≥ 18.13Go ≥ 1.13
Đã chạy thử trên
Node.js 24.12.0Go 1.27.1

Code Node.js là ES module: lưu file .mjs hoặc đặt "type": "module" trong package.json.

Node.js có fetch() toàn cục (dựng trên undici) — cùng API với trình duyệt, trả về Promise<Response>. Go dùng net/http trong thư viện chuẩn: http.Get cho trường hợp đơn giản, http.NewRequestWithContext + Client.Do khi cần method, header hay context riêng. Hai bên giống nhau ở một điểm hay bị hiểu nhầm: response 4xx/5xx không phải lỗi — chỉ lỗi mạng (DNS, connection refused, timeout…) mới khiến fetch() reject hay err != nil trong Go. Các ví dụ dưới gọi JSONPlaceholder, một API giả lập công khai trả về dữ liệu cố định, nên output // → lặp lại được; riêng phần timeout tự dựng một server chậm ngay trong chương trình để không phụ thuộc mạng.

GET và đọc JSON

const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
console.log(res.status); // → 200
console.log(res.headers.get("content-type")); // → application/json; charset=utf-8
 
const todo = await res.json(); // đọc body và parse JSON
console.log(todo);
// → { userId: 1, id: 1, title: 'delectus aut autem', completed: false }

POST JSON kèm header

const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Accept": "application/json",
  },
  body: JSON.stringify({ title: "hello", userId: 1 }), // body nhận string, Buffer/TypedArray/ArrayBuffer, Blob, FormData, URLSearchParams hoặc stream; object thường bị ép thành "[object Object]", không phải JSON
});
 
console.log(res.status); // → 201
console.log(await res.json()); // → { title: 'hello', userId: 1, id: 101 }

Mã lỗi 4xx/5xx không phải lỗi

const res = await fetch("https://jsonplaceholder.typicode.com/todos/9999");
console.log(res.ok, res.status); // → false 404 (không throw!)
 
if (!res.ok) {
  console.log(`HTTP ${res.status} ${res.statusText}`); // → HTTP 404 Not Found
}
 
try {
  await fetch("http://does-not-exist.invalid"); // tên miền không tồn tại: lỗi mạng thật
} catch (err) {
  console.log(err.name, err.message); // → TypeError fetch failed
  console.log(err.cause.code); // → ENOTFOUND
}

Timeout

import http from "node:http";
import { once } from "node:events";
 
// server giả lập chậm: 2 giây mới trả lời
const server = http.createServer((req, res) => {
  const timer = setTimeout(() => res.end("late"), 2000);
  res.on("close", () => clearTimeout(timer)); // client bỏ đi: huỷ timer để tiến trình thoát ngay
});
server.listen(0); // port 0: để hệ điều hành chọn port trống
await once(server, "listening");
const url = `http://127.0.0.1:${server.address().port}`;
 
try {
  await fetch(url, { signal: AbortSignal.timeout(500) }); // huỷ sau 500ms
} catch (err) {
  console.log(err.name); // → TimeoutError
} finally {
  server.close();
}

Đọc body dạng stream

import { createWriteStream } from "node:fs";
import { rm, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
 
const res = await fetch("https://jsonplaceholder.typicode.com/photos");
 
// res.body là web ReadableStream: ghi dần ra file, không giữ cả body trong RAM
const path = join(tmpdir(), "jsrosetta-photos.json");
try {
  await pipeline(Readable.fromWeb(res.body), createWriteStream(path));
  const { size } = await stat(path);
  console.log(size); // → 1039898 (byte, đã giải nén gzip)
} finally {
  await rm(path, { force: true }); // dọn file tạm
}