Repository navigation
fix(batch): match the Batch client to what the API returns and accepts - #59
Merged
Merged
Conversation
- Add the expired task status. Bulk downloads skip expired and failed rows that have no body and report each through onSkip, instead of failing partway through. - Add getTaskContentBytes, which returns a Buffer so binary results such as PDFs arrive intact. getTaskContent still returns text. - Run waiters no longer poll a paused run until the timeout: they throw RunPausedError with the run, unless waitWhilePaused is set. - Add verifyWebhookSignature, which decodes the base64 secret, picks the key by kid, compares in constant time and checks the timestamp (300 s by default), plus the WebhookEvent type and header constants. - Add BatchErrorCode with every error code the API sends, and detail and retryAfter on ZenRowsBatchError. - addTasks accepts an idempotencyKey, so a transient failure is retried. - rerun, retryFailed and submitScheduled accept waitForIngest. - putJobWebhook no longer sends signature: false when it is left out; it keeps the job's current setting. - A 429 quota_exceeded is an account limit and is no longer retried. - Requests time out after 30 seconds by default (timeout option); they had no limit before. - The cost estimate counts a flag as on only for true or "true", and mode only for "auto", the values the API accepts. - Types: method and body on tasks, created_at and updated_at on results, trigger on runs. TaskResult.spend and the results limit option are deprecated: the API never returns the first and ignores the second.
The old example called methods the client does not have (addTasks, closeJob, rerun on client.batch) and read job.job_id from a JobRef, so it did not compile. It also said schedules, webhooks, HMAC keys, CSV uploads and exports were not wrapped. The section now uses the JobRef API, documents retention, errors and retries, paused runs, and webhook verification, and a test typechecks every code block in it against the source.
…ontent reads - An ingestion wait is done only when ingest_status leaves pending. A run stopped mid-ingest is terminal while the API is still settling its rows and totals, so the run status alone no longer ends the wait. - rerun and submit with waitForIngest poll the run they returned, by id, instead of the job's latest run. A replayed request returns its original run, which may no longer be the latest. - getTaskContent and getTaskContentBytes read the body inside the request timeout, so a body that stalls after the headers no longer hangs. - submitScheduled no longer passes waitForIngest: a scheduled submit creates no run, so there is nothing to wait for. The README says so.
emirlan-zen
approved these changes
Oct 20, 2026
Merged
Contributor
|
Proof on the merged code ( The webhook lines verify a header made by the Batch API's own signing code. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The Batch client had drifted from the Batch API in several places, and the README's Batch example did not compile. This PR brings the client, its types and the README back in line, and adds the helpers the Python and Go SDKs are getting at the same time: webhook signature verification, named error codes and an add-tasks idempotency key.
Nothing breaks: every exported name and signature is kept, removals are deprecations, and
getTaskContent()still returns text. The behaviour changes are listed at the end.Changes
Results
TaskStatusgainsexpired. After the retention window (14 days by default) a successful task comes backexpiredwith noresult_url, and astatus: "successful"filter still matches it.downloadToDir()anddownloadToMemory()failed partway through on the first such row, after some files were already written. They now skipexpiredandfailedrows that have no body, and report each one through a newonSkipoption. Any other row without aresult_urlstill fails the download, so a missing body is never dropped silently.getTaskContentBytes()returns the content as aBuffer.getTaskContent()decodes every body as text, which corrupts apdfor other binary result; its doc comment and the README now say to usegetTaskContentBytes()for those.Webhooks
verifyWebhookSignature(rawBody, header, secrets, options). It:X-Signature: t=...,v1=...,kid=...and picks the secret bykid, so both keys work during a rotation;t + "." + body;toleranceSeconds: 0turns that check off);WebhookEventtype for the delivery body, andWEBHOOK_SIGNATURE_HEADER/WEBHOOK_EVENT_ID_HEADER.putJobWebhook()used to sendsignature: falsewhenever the caller left it out, so changing only the URL turned signing off. Withsignatureomitted, it now reads the job's current setting and sends that back (falsewhen the job has no webhook yet). An explicit value is sent as given, with no extra request.Errors and retries
BatchErrorCodeconst object (andBatchErrorCodeValuetype) with every error code the API sends.ZenRowsBatchErrorgainsdetailandretryAfter, matching the Crawl error.quota_exceededis no longer retried. It is an account limit (active jobs, scheduled jobs, or an open job's task cap) that clears only when a job finishes or is deleted, so retries only added delay. Other 429s are retried as before.job.addTasks(tasks, { idempotencyKey })sendsIdempotency-Key. With a key, a transient failure is retried; without one, behaviour is unchanged.BatchClientConfig.timeout: seconds per API request, default 30, the same default as the Python SDK and the Crawl client. This is a fix: Batch requests had no time limit at all before, so a stalled connection could hang a call forever. The timeout covers reading the body too, sogetTaskContent()andgetTaskContentBytes()can't hang on a body that stalls after the headers. Downloads fromresult_urland waiters are not limited by it.Waiting on a paused run
pause_state: "paused") makes no progress until it is resumed, butwaitForRun(),job.run.wait()andrun.wait()kept polling it until the timeout. They now stop at once and throw a newRunPausedErrorwhoserunis the paused run. A throw matches how the waiters report a timeout (WaiterTimeoutError). PasswaitWhilePaused: trueto keep polling, for example when another process will resume the run. A paused run that is already in a target status still resolves.Waiting for ingestion
rerun()andretryFailed()now acceptwaitForIngest, like the submit methods already did.ingest_statusleavespending. Before, it also ended as soon as the run was terminal, but a run stopped mid-ingest is terminal while the API is still settling its rows and totals.rerun()and the submit methods wait on the run they returned, by id, instead of the job's latest run. A replayed request (sameidempotencyKey) returns its original run, which may no longer be the latest one.submitScheduled()ignoreswaitForIngest. A scheduled submit creates no run: it stores the task template, and the first run starts when the schedule fires. The README says so.Cost estimate
trueor"true"(any case), andmodeonly for"auto". The API rejects anything else with a 400. Before,"1","yes"and non-zero numbers priced a job the API would refuse.Types
methodandbodyon task inputs (POST tasks).created_atandupdated_atonTaskResult.triggeronRun, with a newRunTriggertype.TaskResult.spend/TaskSpendandGetResultsOptions.limitare deprecated. The API never returns the first and ignores the second, which is no longer sent.README
JobRefstyle:submitRegular/submitOpen,job.run.wait(),job.run.results(),downloadToDir,retryFailed,addTasks.node:httpreceiver).extract_fieldsisn't supported in Batch is gone; the API accepts it.tests/readme-batch.test.ts, typechecks every code block in the Batch section againstsrc/. An example that calls a method the client doesn't have now fails CI.Behaviour changes
None of these changes a type or a signature:
timeoutis set.RunPausedErroron a paused run instead of polling until the timeout.putJobWebhook()withoutsignaturekeeps the current setting instead of turning signing off.expiredandfailedrows instead of failing.quota_exceededis not retried.Testing
pnpm check(Biome): clean.tsc --noEmitwithtsconfig.jsonandtsconfig.test.json: clean.vitest run: 477 passed, 6 skipped (the Crawl e2e test, which needs a key), no type errors.pnpm build: CJS, ESM and type declarations build. The new exports are present indist.tests/batch-webhook.test.tscame from a copy of the API's own signing code, not from the helper under test.Replaces #57.