Standalone HTTP Client
WakaSync is an HTTP client built on the Fetch API. It adds request grouping, cancellation, automatic retries, and intelligent response parsing — all through a clean promise-based API.
Getting Started
Include the script and start making requests with the global wakaSync instance:
<script src="wakasync.js"></script>
<script>
const users = await wakaSync.get('/api/users');
</script>
All the standard HTTP methods are available. Methods that send a body (POST, PUT, PATCH) accept data as the second argument — plain objects are automatically serialized to JSON:
// GET
const users = await wakaSync.get('/api/users');
// POST with data (auto-serialized to JSON)
const newUser = await wakaSync.post('/api/users', {
name: 'Alice',
email: 'alice@example.com'
});
// PUT, PATCH, DELETE, HEAD
await wakaSync.put('/api/users/123', { name: 'Alice Smith' });
await wakaSync.patch('/api/users/123', { lastLogin: new Date().toISOString() });
await wakaSync.delete('/api/users/123');
await wakaSync.head('/api/users/123');
For anything beyond the defaults, create a configured instance. This is the recommended approach — it keeps your configuration scoped and reusable:
const http = wakaSync.create({
timeout: 10000,
retries: 2,
headers: {
'Authorization': 'Bearer my-token',
'X-API-Version': '2'
}
});
const user = await http.get('/api/user');
You can derive new instances from existing ones. Headers are deep-merged, so the child inherits headers from the parent while adding its own:
const adminApi = http.create({
headers: { 'X-Admin': 'true' }
});
// adminApi sends Authorization, X-API-Version, AND X-Admin
Headers
Headers can be set at two levels: instance defaults and per-request. Per-request headers are merged with defaults, not replaced, so you can add a one-off header without losing your defaults:
const http = wakaSync.create({
headers: {
'Authorization': 'Bearer my-token'
}
});
// Sends both Authorization and X-Request-Id
const data = await http.get('/api/data', {
headers: { 'X-Request-Id': 'abc-123' }
});
// Override Authorization for just this request
const admin = await http.get('/api/admin', {
headers: { 'Authorization': 'Bearer admin-token' }
});
For headers that change between requests — like tokens that expire — use a request interceptor instead. Inside interceptors, config.headers is a Headers instance, so use .set():
http.addRequestInterceptor(function(config) {
config.headers.set('Authorization', 'Bearer ' + getToken());
return config;
});
WakaSync also sets several headers automatically: Content-Type based on the body type (JSON, text, binary, or omitted for FormData so the browser can set the multipart boundary), an Accept header, and tracking headers X-WakaSync-Request and X-WakaSync-Version. All of these can be overridden through your own headers.
Cancellation
Stale responses are a common source of bugs — a slow first request arriving after a fast second one. WakaSync solves this with request groups. Assign a groupKey, and any new request in that group automatically cancels the previous one:
async function search(query) {
try {
const results = await http.get('/api/search?q=' + encodeURIComponent(query), {
groupKey: 'search'
});
renderResults(results);
} catch (err) {
if (!http.isCancellationError(err)) {
console.error('Search failed:', err);
}
}
}
You can also cancel groups manually, cancel everything at once, or check what's in flight:
http.cancelGroup('search');
http.cancelAll();
const count = http.getActiveRequestCount();
For an even simpler pattern, latestOnly uses the request URL as the group key automatically. Repeated calls to the same endpoint keep only the most recent:
http.get('/api/status', { latestOnly: true });
http.get('/api/status', { latestOnly: true }); // first is cancelled
If you need external control over cancellation, pass your own AbortController. WakaSync combines it with its internal controller, so both work independently:
const controller = new AbortController();
http.get('/api/data', { abortController: controller });
controller.abort(); // cancel manually
Retries
WakaSync can retry failed requests automatically. By default it retries on network errors, 5xx server errors, and 429 (rate limiting) — but not 4xx client errors, since those indicate the request itself is wrong:
const http = wakaSync.create({
retries: 3,
retryDelay: 1000,
retryBackoff: 'exponential'
});
const data = await http.get('/api/unstable-endpoint');
The backoff strategy controls delay between attempts: 'exponential' (default, with jitter to prevent thundering herd), 'linear', or 'fixed'. All strategies are capped by retryBackoffMax (default 30s). On 429 responses, WakaSync respects the Retry-After header if present.
Override the default retry logic when you need to be more selective:
const data = await http.get('/api/data', {
retries: 3,
shouldRetry: function(error, attempt, maxAttempts) {
return error.response && error.response.status === 503;
}
});
Interceptors
Interceptors let you hook into the request/response pipeline globally. Both addRequestInterceptor() and addResponseInterceptor() accept sync or async functions and return an unsubscribe function:
// Async request interceptor: refresh expired tokens
http.addRequestInterceptor(async function(config) {
if (isTokenExpired()) {
await refreshToken();
config.headers.set('Authorization', 'Bearer ' + getToken());
}
return config;
});
// Response interceptor with timing metadata
http.addResponseInterceptor(function(data, config, timing) {
console.log(config.method + ' ' + config.url + ' — ' + timing.duration + 'ms');
return data;
});
// Remove an interceptor later
const unsubscribe = http.addResponseInterceptor(function(data) {
return Object.assign({}, data, { _receivedAt: Date.now() });
});
unsubscribe();
Interceptors should return the modified config or data. If nothing is returned (undefined), the original value is preserved. Any other return value — including falsy ones like 0, "", false, or null — replaces it.
By default, derived instances created with .create() do not inherit interceptors. Pass copyInterceptors: true to carry them over:
const adminApi = http.create(
{ headers: { 'X-Admin': 'true' } },
{ copyInterceptors: true }
);
Error Handling
WakaSync errors always include an error.code property for programmatic handling, and HTTP errors attach the original Response object as error.response:
try {
const data = await http.get('/api/data');
} catch (err) {
if (http.isCancellationError(err)) {
return; // expected — don't show error UI
}
if (err.response) {
// HTTP error: err.code is 'HTTP_404', 'HTTP_500', etc.
console.error('HTTP ' + err.response.status + ':', err.message);
} else {
// Network error, parse error, or invalid config
console.error('Error:', err.message, err.code);
}
}
The isCancellationError() helper catches all cancellation types in one check — timeouts, manual cancellation, and superseded requests. Use it to distinguish expected cancellations from real errors.
Other Features
Response Types
WakaSync auto-detects the response format from the Content-Type header. Override with responseType when you need specific handling:
const blob = await http.get('/api/download', { responseType: 'blob' });
const raw = await http.get('/api/stream', { responseType: 'response' });
const html = await http.get('/api/page', { responseType: 'text' });
Status Validation
By default, any non-ok status (outside 200–299) throws an error. Override validateStatus to change this:
const data = await http.get('/api/optional', {
validateStatus: function(response) {
return response.ok || response.status === 404;
}
});
File Uploads
Pass FormData as the body — WakaSync skips setting Content-Type so the browser can set the correct multipart boundary:
const formData = new FormData();
formData.append('file', file);
const result = await http.post('/api/upload', formData);
Fetch Passthrough Options
Options like credentials, mode, cache, redirect, referrer, referrerPolicy, integrity, keepalive, and priority are passed directly through to the underlying fetch() call:
const data = await http.get('https://api.example.com/data', {
credentials: 'include',
mode: 'cors',
cache: 'no-store'
});
Reference
Configuration Options
All options can be set as instance defaults via wakaSync.create() and overridden per-request.
| Option | Default | Description |
|---|---|---|
timeout |
30000 | Request timeout in milliseconds. Set to 0 to disable. |
headers |
{} | Default headers (deep-merged with per-request headers) |
responseType |
'auto' | 'json', 'text', 'blob', 'response', or 'auto' |
validateStatus |
response.ok | Function(response) → true if the status is valid |
retries |
0 | Number of retry attempts |
retryDelay |
1000 | Base delay between retries in ms |
retryBackoff |
'exponential' | 'exponential' (with jitter), 'linear', or 'fixed' |
retryBackoffMax |
30000 | Cap on backoff delay in ms |
groupKey |
— | Group identifier for cancellation via cancelGroup() |
latestOnly |
false | Auto-cancel previous requests to the same URL |
ignoreAbort |
false | Resolve with undefined instead of throwing on cancellation |
abortController |
— | External AbortController (combined with internal controller) |
onSuccess |
— | Callback fired before the promise resolves |
onError |
— | Callback fired before the promise rejects |
shouldRetry |
— | Custom function(error, attempt, maxAttempts) for retry logic |
urlNormalizer |
— | Custom function(url, opts) returning a group key for latestOnly |
baseUrl |
— | Base URL for URL normalization in latestOnly grouping |
Error Codes
| Code | Meaning |
|---|---|
INVALID_URL |
Malformed or empty URL |
HTTP_{status} |
HTTP error (e.g. HTTP_404, HTTP_500) |
PARSE_ERROR |
Failed to parse response body |
CANCEL_TIMEOUT |
Request timed out |
CANCEL_CANCELLED |
Manually cancelled |
CANCEL_SUPERSEDED |
Replaced by a newer request in the same group |
INTERCEPTOR_ERROR |
An interceptor threw an error |
INVALID_CALLBACK |
A callback option was not a function |
Best Practices
- Use groupKey for searches to prevent race conditions and stale results.
- Always catch errors and use
isCancellationError()to distinguish expected cancellations from real failures. - Call cancelAll() on teardown to avoid responses arriving after cleanup.
- Use create() for isolation — separate instances for different APIs keep configuration and interceptors scoped.
- Set reasonable timeouts — the 30s default is fine for most APIs, but adjust for your use case.