Start HereOrdered learning track

Request Init, Headers, Body, and Mode

Learn React Client-Server Communication - Part 010

RequestInit, Headers, body, mode, credentials, cache, redirect, referrer, integrity, keepalive, priority, dan signal sebagai control plane request Fetch di React production app.

17 min read3307 words
PrevNext
Lesson 1072 lesson track01–13 Start Here
#react#client-server#fetch-api#requestinit+4 more

Part 010 — Request Init, Headers, Body, and Mode

Part sebelumnya membahas Fetch sebagai pipeline.

Sekarang kita masuk ke control plane-nya: RequestInit.

await fetch('/api/orders', {
  method: 'POST',
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ itemId: 'item_123', quantity: 2 }),
  credentials: 'same-origin',
  mode: 'same-origin',
  cache: 'no-store',
  redirect: 'follow',
  signal: AbortSignal.timeout(10_000),
})

Ini terlihat seperti object konfigurasi biasa. Sebenarnya ini adalah deklarasi ke browser:

  • method apa yang digunakan,
  • metadata apa yang dikirim,
  • body apa yang dikirim,
  • credential boleh ikut atau tidak,
  • cross-origin policy apa yang diterapkan,
  • cache browser boleh dipakai atau tidak,
  • redirect diikuti atau ditolak,
  • referrer dikirim atau disembunyikan,
  • request boleh hidup melewati page unload atau tidak,
  • request bisa dibatalkan kapan,
  • response seperti apa yang boleh terlihat ke JavaScript.

Dengan kata lain:

RequestInit adalah contract antara application intent dan browser network machinery.

Kalau asal mengisi RequestInit, kamu tidak hanya membuat request. Kamu sedang menentukan security boundary, cache behavior, observability, privacy, dan failure semantics.


1. Anatomy of RequestInit

Field utama yang sering relevan untuk React application code:

FieldFungsiBiasanya dipakai untuk
methodHTTP methodGET, POST, PUT, PATCH, DELETE
headersrequest metadataAccept, Content-Type, correlation id, idempotency key
bodyrequest payloadJSON, FormData, Blob, URLSearchParams, stream
modecross-origin request modecors, same-origin, no-cors
credentialscredential sending/receiving policycookie session, cross-origin API
cachebrowser HTTP cache behaviordefault cache, no-store, reload, revalidation
redirectredirect handlingfollow, error, manual
referrerexplicit referrer valueprivacy-sensitive request
referrerPolicyreferrer exposure rulereduce path/query leakage
integritysubresource integrity metadatafetch known static asset securely
keepaliveallow request to outlive pageanalytics/beacon-like fire-and-forget
signalcancellation/deadlineroute change, search debounce, timeout
prioritypriority hint where supportedtuning relative importance

Beberapa field lain ada di platform dan bisa experimental, browser-specific, atau hanya relevan untuk fitur tertentu. Prinsipnya: gunakan yang kamu pahami efek policy-nya. Jangan copy-paste RequestInit dari internet lalu menjadikannya global default.


2. method: Intent, Safety, dan Idempotency

method bukan string kosmetik.

await fetch('/api/orders', { method: 'GET' })
await fetch('/api/orders', { method: 'POST' })
await fetch('/api/orders/ord_123', { method: 'PATCH' })
await fetch('/api/orders/ord_123', { method: 'DELETE' })

HTTP method membawa semantic:

MethodSafe?Idempotent?Typical use
GETYaYaread resource
HEADYaYaread metadata
POSTTidakTidak secara defaultcreate, command, submit
PUTTidakYa secara semanticreplace resource
PATCHTidakTergantung designpartial update
DELETETidakBiasanya idempotent secara intentdelete resource

Safe berarti request tidak dimaksudkan mengubah state server.

Idempotent berarti mengirim request yang sama berkali-kali memiliki efek akhir yang sama. Ini penting untuk retry.

2.1 React implication

Query/data fetching layer boleh lebih agresif untuk GET:

  • dedupe,
  • prefetch,
  • background refetch,
  • retry,
  • cache.

Mutation harus lebih hati-hati:

  • tidak semua POST boleh retry,
  • optimistic update butuh rollback,
  • user confirmation bisa diperlukan,
  • duplicate submission harus dicegah,
  • idempotency key perlu dipakai untuk command sensitif.

2.2 Jangan pakai GET untuk mutation

Buruk:

await fetch('/api/orders/ord_123/cancel')

Masalah:

  • browser/proxy/cache bisa menganggap safe,
  • link previewer bisa memicu action,
  • prefetch bisa mengubah data,
  • retry/dedupe jadi tidak aman,
  • audit trail kabur.

Lebih benar:

await fetch('/api/orders/ord_123/cancellation', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ reason: 'customer_requested' }),
})

Atau jika model resource-nya memang state transition:

await fetch('/api/orders/ord_123', {
  method: 'PATCH',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ status: 'cancelled' }),
})

Pilih berdasarkan domain contract, bukan preferensi frontend.


3. headers: Metadata Contract

headers bisa diberikan sebagai plain object, array tuple, atau Headers instance.

await fetch('/api/orders', {
  headers: {
    Accept: 'application/json',
  },
})
await fetch('/api/orders', {
  headers: [
    ['Accept', 'application/json'],
    ['X-Request-Id', crypto.randomUUID()],
  ],
})
const headers = new Headers()
headers.set('Accept', 'application/json')
headers.set('X-Request-Id', crypto.randomUUID())

await fetch('/api/orders', { headers })

3.1 Header names are case-insensitive

HTTP header names case-insensitive. Namun di code, pilih satu style konsisten.

headers.set('Accept', 'application/json')
headers.get('accept') // tetap bisa bekerja pada Headers object

Dalam DevTools HTTP/2/HTTP/3, header sering terlihat lowercase. Jangan menulis logic yang bergantung pada casing.

3.2 Accept is not Content-Type

Accept menjelaskan response yang client harapkan.

headers: {
  Accept: 'application/json',
}

Content-Type menjelaskan request body.

headers: {
  'Content-Type': 'application/json',
}

Untuk GET, jangan asal set Content-Type karena tidak ada body.

await fetch('/api/orders', {
  headers: {
    Accept: 'application/json',
  },
})

Untuk JSON mutation:

await fetch('/api/orders', {
  method: 'POST',
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ itemId: 'item_123' }),
})

3.3 Forbidden request headers

Browser tidak mengizinkan JavaScript mengatur semua header.

Contoh kategori yang dikontrol browser:

  • Host,
  • Connection,
  • beberapa cookie-related header,
  • beberapa CORS/fetch metadata header,
  • beberapa transport-level header.

Kenapa?

Karena page JavaScript berjalan dalam sandbox. Browser harus menjaga agar page tidak bisa memalsukan metadata transport/security yang dipakai untuk policy enforcement.

Jadi kalau ini tidak bekerja:

await fetch('/api/orders', {
  headers: {
    Host: 'evil.example.com',
    Cookie: 'session=abc',
  },
})

Itu bukan bug React. Itu browser security model.

3.4 CORS-safelisted request headers

Untuk cross-origin request, beberapa header dianggap sederhana/safelisted jika memenuhi constraint tertentu, misalnya:

  • Accept,
  • Accept-Language,
  • Content-Language,
  • Content-Type dengan value tertentu,
  • Range dengan constraint tertentu.

Header custom seperti ini biasanya memicu preflight:

await fetch('https://api.example.com/orders', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Request-Id': crypto.randomUUID(),
  },
  body: JSON.stringify({ itemId: 'item_123' }),
})

Preflight bukan error. Preflight adalah browser bertanya ke server:

“Origin ini boleh mengirim method/header ini?”

Kalau server/API gateway tidak menangani preflight, request actual tidak akan dikirim.

3.5 Response headers may be hidden

Cross-origin response tidak otomatis mengekspos semua headers ke JavaScript.

Misalnya backend mengirim:

X-Request-Id: req_123
X-RateLimit-Remaining: 42

Tetapi client:

response.headers.get('x-request-id') // null pada CORS response jika tidak exposed

Server perlu:

Access-Control-Expose-Headers: X-Request-Id, X-RateLimit-Remaining

Kalau observability kamu bergantung pada X-Request-Id, pastikan header itu exposed untuk cross-origin frontend.


4. body: Payload Bukan Selalu JSON

body adalah request payload.

Common body types:

Body typeUse caseContent-Type behavior
stringJSON string, textkamu biasanya set manual
URLSearchParamsform-urlencodedbrowser bisa set application/x-www-form-urlencoded;charset=UTF-8
FormDatafile upload/formbrowser set multipart boundary
Blobbinary payloadtergantung blob type
ArrayBuffer / typed arraysbinary protocolset sesuai protocol
ReadableStreamstreaming uploadadvanced/runtime-dependent

4.1 JSON body

await fetch('/api/orders', {
  method: 'POST',
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ itemId: 'item_123', quantity: 2 }),
})

Pitfall: JSON.stringify(undefined) menghasilkan undefined, bukan string JSON valid.

JSON.stringify(undefined) // undefined

Kalau wrapper kamu menerima json?: unknown, bedakan antara “tidak ada body” dan “body JSON null”.

type JsonRequest =
  | { json: unknown; body?: never }
  | { body: BodyInit; json?: never }
  | { json?: never; body?: never }

4.2 null vs empty body

body: JSON.stringify(null) // "null"

Berbeda dari:

body: undefined // no body

Server contract harus jelas. Untuk mutation command, empty body dan JSON null sebaiknya tidak ambigu.

4.3 FormData

const form = new FormData()
form.append('title', title)
form.append('file', file)

await fetch('/api/documents', {
  method: 'POST',
  body: form,
})

Jangan set Content-Type manual.

// salah untuk FormData
headers: {
  'Content-Type': 'multipart/form-data',
}

Browser perlu membuat boundary.

4.4 URLSearchParams

Untuk form-urlencoded:

const params = new URLSearchParams()
params.set('username', username)
params.set('password', password)

await fetch('/login', {
  method: 'POST',
  body: params,
})

Ini berguna untuk endpoint legacy, OAuth/token style endpoints tertentu, atau form-compatible backend.

4.5 GET/HEAD body

Jangan mengirim body untuk GET/HEAD.

await fetch('/api/orders', {
  method: 'GET',
  body: JSON.stringify({ status: 'paid' }), // invalid/problematic
})

Gunakan query params:

const url = new URL('/api/orders', window.location.origin)
url.searchParams.set('status', 'paid')

await fetch(url, { method: 'GET' })

5. mode: Browser Cross-Origin Contract

mode menentukan cross-origin behavior.

Common values:

ModeMeaningUse case
same-originhanya izinkan same-origininternal app API yang tidak boleh lintas origin
corsizinkan cross-origin dengan CORSAPI lintas origin yang benar
no-corsrequest terbatas, response opaquebeacon/assets tertentu, bukan membaca API JSON

5.1 same-origin

await fetch('/api/orders', {
  mode: 'same-origin',
})

Kalau URL berubah menjadi cross-origin, browser menolak.

Ini bagus untuk API internal yang seharusnya tidak pernah keluar origin. Ia bisa menjadi guardrail.

5.2 cors

await fetch('https://api.example.com/orders', {
  mode: 'cors',
})

Ini default untuk banyak cross-origin fetch. Browser akan menerapkan CORS policy.

CORS bukan auth. CORS adalah browser read-access policy.

Server tetap harus melakukan authentication/authorization. Jangan pernah menganggap CORS sebagai security utama untuk API.

5.3 no-cors

no-cors sering disalahgunakan.

const response = await fetch('https://api.example.com/orders', {
  mode: 'no-cors',
})

console.log(response.type)   // opaque
console.log(response.status) // 0

Response opaque tidak memberikan body/status/header yang berguna ke JavaScript.

Gunakan no-cors hanya kalau kamu memang tidak perlu membaca response. Untuk API JSON React app, hampir selalu salah.

5.4 Mode as architecture signal

Untuk internal same-origin BFF:

fetch('/api/orders', {
  mode: 'same-origin',
  credentials: 'same-origin',
})

Untuk public API cross-origin tanpa cookies:

fetch('https://api.example.com/orders', {
  mode: 'cors',
  credentials: 'omit',
})

Untuk cross-origin cookie session:

fetch('https://api.example.com/me', {
  mode: 'cors',
  credentials: 'include',
})

Masing-masing membutuhkan server policy berbeda.


credentials mengontrol apakah browser mengirim credential dan apakah browser menghormati Set-Cookie response tertentu.

Values:

ValueMeaning
omitjangan kirim credentials
same-originkirim untuk same-origin
includekirim juga untuk cross-origin jika policy mengizinkan
await fetch('/api/me', {
  credentials: 'same-origin',
})

Ini cocok untuk BFF/same-origin deployment:

await fetch('https://api.example.com/me', {
  mode: 'cors',
  credentials: 'include',
})

Server harus mengirim CORS headers yang tepat:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

Dan cookie harus punya attribute yang sesuai untuk konteks cross-site jika memang cross-site.

6.3 Bearer token case

Kalau memakai bearer token manual:

await fetch('https://api.example.com/me', {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
  credentials: 'omit',
})

Tapi jangan menyimpulkan bearer token selalu lebih aman. Token storage, refresh, XSS exposure, rotation, and revocation adalah desain lain. Seri auth membahas lebih dalam; di sini fokus kita adalah transport behavior.

6.4 Rule of thumb

  • Same-origin app API: credentials: 'same-origin'.
  • Public third-party API tanpa cookies: credentials: 'omit'.
  • Cross-origin cookie API: credentials: 'include' plus CORS credentials server-side.
  • Jangan global default include tanpa alasan kuat.

7. cache: Browser HTTP Cache Control

cache memberi sinyal bagaimana request berinteraksi dengan browser HTTP cache.

Common values:

ValueRough meaning
defaultgunakan normal HTTP cache semantics
no-storejangan pakai/simpan HTTP cache
reloadbypass cache untuk request, bisa update cache
no-cacherevalidate sebelum pakai cached response
force-cachegunakan cache jika ada walau stale dalam beberapa kondisi
only-if-cachedhanya cache; punya constraint mode tertentu

7.1 Jangan campuradukkan cache browser dan query cache

React app bisa punya beberapa cache layer:

cache: 'no-store' hanya memengaruhi Fetch/browser HTTP cache behavior. Itu tidak otomatis menghapus TanStack Query cache atau framework data cache.

7.2 API data biasa

Untuk most authenticated API data, banyak tim memilih:

await fetch('/api/orders', {
  cache: 'no-store',
})

Lalu application cache dikelola oleh query/router layer.

Tapi ini bukan aturan absolut.

Untuk static/config/versioned resources, HTTP cache bisa sangat efektif.

await fetch('/config/app-config.v42.json', {
  cache: 'force-cache',
})

7.3 Revalidation-aware request

Kalau server mengirim ETag, browser bisa revalidate.

ETag: "orders-v123"
Cache-Control: max-age=0, must-revalidate

Client dengan cache: 'default' atau no-cache bisa memanfaatkan conditional request tergantung browser/cache state.

Namun di React application architecture, jangan mengandalkan implicit browser cache untuk domain consistency kalau query cache juga aktif. Pilih ownership.

7.4 only-if-cached

only-if-cached punya constraint ketat dan biasanya hanya bekerja dengan mode: 'same-origin'. Ini jarang diperlukan dalam application code biasa.

Kalau kamu ingin offline-first behavior, Service Worker + Cache Storage biasanya lebih eksplisit daripada berharap fetch cache mode menyelesaikan semuanya.


8. redirect: Follow, Error, Manual

redirect mengontrol bagaimana fetch menangani HTTP redirect.

Values:

ValueMeaning
followbrowser mengikuti redirect
errorredirect membuat fetch reject
manualexpose redirect terbatas/filtered tergantung mode/runtime

Default umumnya follow.

await fetch('/api/orders', {
  redirect: 'follow',
})

8.1 Redirect can hide auth problems

Misalnya API mengembalikan redirect ke login HTML:

302 Location: /login

Fetch follow redirect, lalu kamu menerima HTML login page dengan status 200.

Kemudian:

await response.json() // parse error

Dari UI terlihat “JSON parse failed”, padahal akar masalahnya session expired atau gateway redirect.

Untuk API, lebih sehat server mengembalikan:

401 Unauthorized
Content-Type: application/problem+json

Bukan redirect HTML.

8.2 Use redirect error for strict API client

Untuk internal API client, kamu bisa memilih:

await fetch('/api/orders', {
  redirect: 'error',
})

Ini membuat redirect menjadi failure cepat. Cocok jika API contract melarang redirect.

Trade-off: beberapa deployment/gateway mungkin melakukan canonical redirect (http ke https, trailing slash, region redirect). Pastikan contract infrastruktur jelas sebelum menjadikan ini default.

8.3 Manual redirect is not full access

redirect: 'manual' di browser tidak memberi kamu full redirect response seperti server-side HTTP client. Browser tetap menerapkan filtered response untuk security.

Jangan membangun auth/navigation protocol kompleks berdasarkan asumsi bahwa browser fetch bisa membaca semua redirect detail lintas origin.


9. referrer and referrerPolicy

Referrer bisa membocorkan informasi.

Contoh user berada di:

https://app.example.com/cases/case_123?token=abc

Lalu browser melakukan request ke third-party asset/API. Tanpa policy yang tepat, sebagian URL asal bisa terkirim sebagai referrer, tergantung default browser/policy.

Gunakan referrerPolicy untuk request sensitif.

await fetch('https://analytics.example.net/event', {
  method: 'POST',
  referrerPolicy: 'no-referrer',
  body: JSON.stringify(event),
})

Common policies:

PolicyMeaning singkat
no-referrerjangan kirim referrer
originkirim origin saja
same-originkirim referrer hanya same-origin
strict-originkirim origin untuk HTTPS→HTTPS, tidak untuk downgrade
strict-origin-when-cross-originumum sebagai default modern

Untuk regulatory/case management app, hindari menaruh sensitive identifier di URL jika tidak perlu. Referrer policy adalah mitigation, bukan alasan untuk membuat URL bocor.


10. integrity: Subresource Integrity for Fetch

integrity memungkinkan browser memverifikasi content hash untuk resource tertentu.

await fetch('/assets/config.json', {
  integrity: 'sha384-...',
})

Ini lebih umum untuk static assets/subresources daripada API data dinamis.

Kapan berguna?

  • static configuration pinned by hash,
  • third-party static resource,
  • high-integrity bootstrap resource,
  • supply-chain-sensitive asset.

Kapan tidak cocok?

  • API JSON dinamis,
  • user-specific data,
  • frequently changing response tanpa hash management,
  • response terkompresi/ditransformasi di jalur yang membuat hash mismatch.

11. keepalive: Request yang Boleh Melewati Page Unload

keepalive memungkinkan request tertentu tetap berjalan saat page unload/navigation.

Use case:

  • analytics event,
  • session end signal,
  • lightweight audit ping,
  • best-effort telemetry.
await fetch('/api/telemetry/page-exit', {
  method: 'POST',
  body: JSON.stringify({ path: location.pathname }),
  headers: {
    'Content-Type': 'application/json',
  },
  keepalive: true,
})

Tapi jangan pakai keepalive untuk critical mutation.

Buruk:

fetch('/api/payments', {
  method: 'POST',
  body: JSON.stringify(payment),
  keepalive: true,
})

Kenapa?

  • ukuran body dibatasi,
  • lifecycle best-effort,
  • user feedback tidak jelas,
  • retry/confirmation sulit,
  • duplicate/partial processing bisa sulit ditangani,
  • tidak cocok untuk flow bisnis kritis.

Untuk critical mutation, buat UX yang eksplisit: pending, success/failure, retry, idempotency, audit trail.


12. signal: Cancellation and Deadline

signal menerima AbortSignal.

const controller = new AbortController()

await fetch('/api/orders', {
  signal: controller.signal,
})

12.1 Component-owned request

useEffect(() => {
  const controller = new AbortController()

  getOrder(orderId, { signal: controller.signal })
    .then(setOrder)
    .catch((error) => {
      if (error instanceof DOMException && error.name === 'AbortError') return
      setError(error)
    })

  return () => controller.abort()
}, [orderId])

12.2 Query-owned request

useQuery({
  queryKey: ['orders', orderId],
  queryFn: ({ signal }) => getOrder(orderId, { signal }),
})

12.3 Timeout/deadline

await fetch('/api/orders', {
  signal: AbortSignal.timeout(10_000),
})

Timeout adalah client-side deadline. Ia tidak menjamin server menghentikan processing. Untuk mutation, server masih bisa memproses setelah client abort.

Jadi:

  • timeout query biasanya aman untuk retry,
  • timeout mutation harus dipadukan dengan idempotency key dan status reconciliation.

13. Building a Typed Request Builder

Tujuan: cegah kombinasi invalid.

Kita ingin API seperti ini:

api.requestJson({
  method: 'POST',
  path: '/api/orders',
  json: { itemId: 'item_123' },
  idempotencyKey: crypto.randomUUID(),
  signal,
})

Bukan:

fetch('/api/orders', {
  method: 'GET',
  body: JSON.stringify({ status: 'paid' }),
  headers: {
    'Content-Type': 'multipart/form-data',
  },
})

13.1 Types

type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'

type BodylessMethod = 'GET' | 'HEAD'
type BodyAllowedMethod = Exclude<HttpMethod, BodylessMethod>

type BaseRequestOptions = {
  path: string
  headers?: HeadersInit
  signal?: AbortSignal
  credentials?: RequestCredentials
  mode?: RequestMode
}

type BodylessRequestOptions = BaseRequestOptions & {
  method: BodylessMethod
  query?: Record<string, string | number | boolean | null | undefined>
  json?: never
  body?: never
}

type JsonRequestOptions = BaseRequestOptions & {
  method: BodyAllowedMethod
  query?: Record<string, string | number | boolean | null | undefined>
  json: unknown
  body?: never
}

type RawBodyRequestOptions = BaseRequestOptions & {
  method: BodyAllowedMethod
  query?: Record<string, string | number | boolean | null | undefined>
  body: BodyInit
  json?: never
}

type RequestOptions =
  | BodylessRequestOptions
  | JsonRequestOptions
  | RawBodyRequestOptions

13.2 URL builder

function buildUrl(
  baseUrl: string,
  path: string,
  query?: Record<string, string | number | boolean | null | undefined>,
): URL {
  const url = new URL(path, baseUrl)

  for (const [key, value] of Object.entries(query ?? {})) {
    if (value === null || value === undefined) continue
    url.searchParams.set(key, String(value))
  }

  return url
}

13.3 Init builder

function buildRequestInit(options: RequestOptions): RequestInit {
  const headers = new Headers(options.headers)
  headers.set('Accept', headers.get('Accept') ?? 'application/json')

  let body: BodyInit | undefined

  if ('json' in options) {
    headers.set('Content-Type', headers.get('Content-Type') ?? 'application/json')
    body = JSON.stringify(options.json)
  } else if ('body' in options) {
    body = options.body
  }

  return {
    method: options.method,
    headers,
    body,
    signal: options.signal,
    mode: options.mode,
    credentials: options.credentials,
  }
}

Masalah subtle: headers.get('Accept') ?? 'application/json' aman, tetapi Headers.set butuh string. Jika header sudah ada dengan casing berbeda, Headers akan normalize.

13.4 Client

export function createHttpClient(config: {
  baseUrl: string
  defaultCredentials?: RequestCredentials
  defaultMode?: RequestMode
}) {
  return {
    async request(options: RequestOptions): Promise<Response> {
      const url = buildUrl(config.baseUrl, options.path, options.query)
      const init = buildRequestInit({
        ...options,
        credentials: options.credentials ?? config.defaultCredentials,
        mode: options.mode ?? config.defaultMode,
      } as RequestOptions)

      return fetch(url, init)
    },
  }
}

Ini masih simplified. Di production, hindari cast jika bisa dengan overload/helper lebih rapi. Tapi ide besarnya: shape request membatasi misuse.


14. CORS Preflight Decision Model

Preflight terjadi jika request cross-origin tidak memenuhi kriteria “simple request”.

Simplified model:

Important consequences:

  1. Adding Authorization usually triggers preflight.
  2. Adding custom X-* headers usually triggers preflight.
  3. Content-Type: application/json on cross-origin POST usually triggers preflight.
  4. Preflight failure appears to JavaScript as fetch failure, not as readable HTTP response.
  5. Browser may cache successful preflight based on server policy.

Jangan optimize dengan menghilangkan header penting hanya untuk menghindari preflight. Optimalkan server/gateway CORS handling.


15. RequestInit Defaults: What Should Be Global?

Tidak semua option cocok jadi default global.

15.1 Reasonable defaults for same-origin app API

const defaultInit: RequestInit = {
  mode: 'same-origin',
  credentials: 'same-origin',
  redirect: 'error',
  cache: 'no-store',
}

Cocok jika:

  • API memang same-origin,
  • API tidak seharusnya redirect,
  • server-state cache dikelola app/query layer,
  • data user-specific.

Tidak cocok jika:

  • app perlu fetch CDN/static resource,
  • API lintas origin,
  • redirect adalah bagian dari infrastructure,
  • HTTP cache sengaja dipakai.

15.2 Reasonable defaults for cross-origin API without cookies

const defaultInit: RequestInit = {
  mode: 'cors',
  credentials: 'omit',
  redirect: 'error',
  cache: 'no-store',
}

15.3 Avoid these global defaults

Hindari:

credentials: 'include'

sebagai default global tanpa alasan. Ini memperluas credential surface.

Hindari:

mode: 'no-cors'

untuk API. Hampir selalu salah.

Hindari:

cache: 'reload'

sebagai default global. Bisa menghancurkan performa dan meniadakan caching yang valid.

Hindari:

redirect: 'follow'

untuk API yang seharusnya tidak redirect, jika redirect HTML login sering menyamarkan auth failure.


16. RequestInit by Use Case

16.1 Read authenticated same-origin JSON

await fetch('/api/orders?status=paid', {
  method: 'GET',
  mode: 'same-origin',
  credentials: 'same-origin',
  cache: 'no-store',
  headers: {
    Accept: 'application/json',
  },
  signal,
})

16.2 Create resource with JSON

await fetch('/api/orders', {
  method: 'POST',
  mode: 'same-origin',
  credentials: 'same-origin',
  cache: 'no-store',
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({ itemId: 'item_123', quantity: 2 }),
  signal,
})

16.3 Upload file

const form = new FormData()
form.append('file', file)
form.append('documentType', 'evidence')

await fetch('/api/documents', {
  method: 'POST',
  mode: 'same-origin',
  credentials: 'same-origin',
  body: form,
  signal,
})

No manual Content-Type.

16.4 Cross-origin bearer token API

await fetch('https://api.example.com/orders', {
  method: 'GET',
  mode: 'cors',
  credentials: 'omit',
  headers: {
    Accept: 'application/json',
    Authorization: `Bearer ${accessToken}`,
  },
  signal,
})

Expect preflight.

await fetch('https://api.example.com/me', {
  method: 'GET',
  mode: 'cors',
  credentials: 'include',
  headers: {
    Accept: 'application/json',
  },
  signal,
})

Server must allow specific origin and credentials.

16.6 Best-effort telemetry

fetch('/api/telemetry', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ type: 'page_hidden', at: Date.now() }),
  keepalive: true,
})

Do not block navigation on this. Do not use for critical mutation.


17. Security and Privacy Review

Every RequestInit should pass these questions.

17.1 Credentials

  • Does this request need cookies/credentials?
  • Is include necessary, or would same-origin/omit be safer?
  • Is CORS server policy explicit, not wildcard with credentials?
  • Are cookies configured with appropriate SameSite, Secure, and HttpOnly attributes?

17.2 Headers

  • Are we sending unnecessary custom headers that expand preflight surface?
  • Are correlation/request IDs safe to expose?
  • Are we trying to set forbidden headers?
  • Are PII or secrets accidentally placed in headers?

17.3 Body

  • Is body schema validated before send?
  • Is file upload size/type checked client-side and server-side?
  • Are sensitive fields sent only when required?
  • Are idempotency keys used for retryable commands?

17.4 Referrer

  • Could URL path/query leak case IDs, tokens, or user identifiers?
  • Should this request use stricter referrerPolicy?
  • Should sensitive data be removed from URL design entirely?

17.5 Redirect

  • Can API redirect to HTML login and break JSON parsing?
  • Should redirects be treated as error for API calls?
  • Are cross-origin redirects expected?

18. Observability Fields in RequestInit

A mature client often injects correlation headers.

function createRequestHeaders(input?: HeadersInit): Headers {
  const headers = new Headers(input)

  if (!headers.has('Accept')) {
    headers.set('Accept', 'application/json')
  }

  if (!headers.has('X-Request-Id')) {
    headers.set('X-Request-Id', crypto.randomUUID())
  }

  return headers
}

But think carefully:

  • Some organizations prefer W3C traceparent generated by tracing instrumentation.
  • Some proxies overwrite request IDs.
  • Some CORS responses must expose the final response request ID.
  • Client-generated IDs and server-generated IDs may differ.

For debugging production bugs, best shape is often:

type NetworkEvent = {
  operation: string
  method: string
  urlTemplate: string
  status?: number
  durationMs: number
  requestId?: string
  responseRequestId?: string
  errorKind?: string
}

Avoid logging full URL if query contains sensitive data. Prefer route template:

/api/orders/:orderId

instead of:

/api/orders/ord_123?customerEmail=alice@example.com

19. A Safer createRequest Helper

type CreateRequestInput = {
  baseUrl: string
  path: string
  method: HttpMethod
  query?: Record<string, string | number | boolean | null | undefined>
  headers?: HeadersInit
  json?: unknown
  body?: BodyInit
  mode?: RequestMode
  credentials?: RequestCredentials
  cache?: RequestCache
  redirect?: RequestRedirect
  referrerPolicy?: ReferrerPolicy
  keepalive?: boolean
  signal?: AbortSignal
}

function createRequest(input: CreateRequestInput): Request {
  if ((input.method === 'GET' || input.method === 'HEAD') && (input.body || 'json' in input)) {
    throw new Error(`${input.method} request must not include a body`)
  }

  if (input.body && 'json' in input) {
    throw new Error('Use either body or json, not both')
  }

  const url = buildUrl(input.baseUrl, input.path, input.query)
  const headers = new Headers(input.headers)

  if (!headers.has('Accept')) {
    headers.set('Accept', 'application/json')
  }

  let body: BodyInit | undefined

  if ('json' in input) {
    headers.set('Content-Type', headers.get('Content-Type') ?? 'application/json')
    body = JSON.stringify(input.json)
  } else if (input.body) {
    body = input.body
  }

  return new Request(url, {
    method: input.method,
    headers,
    body,
    mode: input.mode,
    credentials: input.credentials,
    cache: input.cache,
    redirect: input.redirect,
    referrerPolicy: input.referrerPolicy,
    keepalive: input.keepalive,
    signal: input.signal,
  })
}

Use it:

const request = createRequest({
  baseUrl: window.location.origin,
  path: '/api/orders',
  method: 'POST',
  json: { itemId: 'item_123' },
  credentials: 'same-origin',
  mode: 'same-origin',
  redirect: 'error',
  cache: 'no-store',
  signal,
})

const response = await fetch(request)

This helper protects a few invariants:

  • no body for GET/HEAD,
  • no mixed json and raw body,
  • default Accept,
  • correct JSON Content-Type,
  • URL encoding through builder,
  • explicit policy fields.

20. Failure Modes by Field

FieldBad value/patternFailure mode
methodGET for mutationaccidental state change via prefetch/cache/link
headerscustom headers cross-originpreflight not handled by server
headersmanual Content-Type for FormDataserver cannot parse multipart boundary
bodybody with GETinvalid/problematic request
modeno-cors for API JSONopaque response; body unreadable
credentialsglobal includeexpanded credential exposure
cacheglobal reloaddegraded performance
cacheimplicit browser cache + query cacheconfusing consistency behavior
redirectfollow API redirect to login HTMLJSON parse error masks auth issue
referrerPolicydefault with sensitive URLspotential referrer leakage
keepalivecritical mutationuncertain lifecycle and feedback
signalomitted for owned requeststale response/state write/leak

21. Implementation Exercise: Request Policy Matrix

Buat matrix policy untuk aplikasi kamu.

Contoh:

Operation classmodecredentialscacheredirectretryidempotency
Same-origin readsame-originsame-originno-storeerroryesn/a
Same-origin mutationsame-originsame-originno-storeerrorlimitedrequired for command
File uploadsame-originsame-originno-storeerrorno/limitedrecommended
Public cross-origin readcorsomitdefaultfollow/erroryesn/a
Cross-origin cookie readcorsincludeno-storeerroryesn/a
Telemetrysame-origin/corsdependsno-storefollownoevent id

Tugas:

  1. Tentukan default per operation class, bukan global tunggal.
  2. Tulis alasan setiap default.
  3. Tandai option yang tidak boleh dioverride oleh feature code.
  4. Tandai option yang boleh dioverride dengan review.
  5. Tentukan telemetry minimal untuk setiap operation class.

22. Ringkasan

RequestInit adalah tempat React app menyatakan policy request kepada browser.

Hal yang harus diingat:

  • method menentukan safety/idempotency expectation.
  • headers adalah metadata contract dan bisa memicu CORS preflight.
  • body harus sesuai method dan content type.
  • mode bukan solusi CORS; no-cors membuat response opaque.
  • credentials memperluas atau membatasi credential surface.
  • cache hanya satu layer dari beberapa cache dalam React app.
  • redirect bisa menyembunyikan auth/gateway problem jika tidak dikontrol.
  • referrerPolicy penting untuk privacy-sensitive URL.
  • keepalive cocok untuk best-effort telemetry, bukan critical mutation.
  • signal adalah lifecycle/deadline boundary.

Request yang baik bukan request yang “berhasil di laptop”. Request yang baik adalah request yang policy-nya eksplisit, failure mode-nya dipahami, dan ownership-nya jelas.

Part berikutnya membahas response parsing dan body streams lebih dalam: bagaimana membaca payload besar, streaming, clone, tee, progress, NDJSON, dan bagaimana menghindari memory trap di React app.


Referensi

  • MDN — RequestInit: https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
  • MDN — Using the Fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
  • MDN — HTTP headers: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers
  • MDN — Forbidden request header: https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_request_header
  • MDN — CORS-safelisted request header: https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_request_header
  • MDN — CORS-safelisted response header: https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header
  • MDN — Request credentials: https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials
  • MDN — Request keepalive: https://developer.mozilla.org/en-US/docs/Web/API/Request/keepalive
  • WHATWG — Fetch Standard: https://fetch.spec.whatwg.org/
Lesson Recap

You just completed lesson 10 in start here. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.