Guide
Sharing identical requests
This page explains share, which merges identical calls made at the same time into one network request. Read it when several parts of an app load the same data at once, or when several failed calls each try to refresh a token.
Several parts of an app often ask for the same thing at the same moment. Five components load the current user, or five calls get a 401 and each one refreshes the token.
share: true merges calls that would send the identical request, with the same URL, method, headers, body and fetch options, into one network request. Each call still runs its own middleware and gets its own Result.
import { createApi, defineRequest } from 'liaise'
type Product = { id: string; name: string }
const getProduct = defineRequest<Product>()({
method: 'GET',
path: '/products/:id',
share: true,
})
const api = createApi({ baseUrl: 'https://api.example.com', requests: { getProduct } })
// One network request. Each caller gets its own Result from it.
const [a, b, c] = await Promise.all([
api.getProduct({ id: '1' }),
api.getProduct({ id: '1' }),
api.getProduct({ id: '1' }),
])
The server sees GET /products/1 once, and a, b and c each get the product in a Result of their own.
What counts as identical
If two requests are the same byte for byte, the server can’t tell them apart either, so sharing them is safe. Apart from tracing headers, if anything differs, they never share.
- The request is compared after all your middleware has run. A header your auth middleware adds is part of it, so calls made as different users don’t share. On a server says what this relies on.
- The URL and the body are compared as sent.
?a=1&b=2and?b=2&a=1don’t share, and neither do two JSON bodies with the same keys in a different order. Params written in the same order always match. - Per-call
headersandmiddlewareare judged by what they change. Calls with identical per-call headers share, and a per-call middleware that changes nothing doesn’t stop sharing. - The
fetchOptionsare compared too, in any key order. A call withcredentials: 'include'never joins one with'omit'. Only string, number, boolean andnullvalues can be compared, so options holding an object, such as Next.js’snext, are never shared (Share key and refcount). - Tracing headers aren’t compared, so a middleware that stamps a request ID on every call doesn’t stop sharing. The shared request goes out with the first caller’s tracing headers. Share key and refcount lists them.
- An upload never shares. A call whose body is
FormData, aBlob, anArrayBuffer, a typed array, aDataViewor aReadableStreamsends its own request.
What each caller gets
- Each caller has its own
datafor JSON and text. A middleware that editsdatachanges only its own caller’s copy (details). onErrorhears about a failed shared request once, however many callers get the error. An error that a caller’s own middleware makes from it is reported separately.- A call that arrives after the shared request has settled sends a new one. Nothing is cached. For that, use
cacheMiddleware. - A per-call
signalortimeoutonly lets that caller leave. The caller that gives up getskind: 'abort'or'timeout'. The request keeps running for the others, and is cancelled once every caller has given up.
const impatient = api.getProduct({ id: '42' }, { timeout: 20 }) // gives up quickly
const patient = api.getProduct({ id: '42' }) // keeps waiting
// impatient's timeout doesn't cancel the shared request, so patient still gets the response.
How the shared request’s own deadline, its timeouts and retries work is under Share key and refcount. A middleware that replaces the signal has its own note, under Signal-replacing middleware.
When not to share
- Think before you set
shareon a write. Two identical writes at the same moment become one, so adding the same item to a cart twice at once adds it once. That suits a refresh-style call, such as the token refresh, and rarely other writes. sharejoins the call already running.dedupecancels it. Setting both on one endpoint throws when you create the client, so you find the mistake straight away.
On a server
One client can serve every user. The user’s token or cookie is part of what is sent, so one user’s call never joins another’s. withHeaders() makes a copy of the client for each user that sends it on every call, and One /me per page view on the server shows the setup.
The comparison sees only what liaise sends: the method, URL, headers, body and fetchOptions. If something outside liaise adds the user’s identity, such as a patched global fetch, a fetch you gave the client that adds it from request context, or instrumentation that reads request context, liaise can’t see it, and share is unsafe in that setup.
Next: Retries, caching and logging covers what liaise has built in: retrying failed calls, caching repeated reads and logging every call.