apify/crawlee · error · Error

At most one of `body`, `form` and `json` may be specified in

Error message

At most one of `body`, `form` and `json` may be specified in sendRequest arguments

What it means

processHttpRequestOptions() converts sendRequest/HttpCrawler request options into a HttpRequest. Only one request payload encoding is allowed: raw `body`, `form`, or `json`. When more than one of these options is defined (not `undefined`), the function aborts because it cannot decide which serialization to use (body stream vs urlencoded vs JSON.stringify) and which content-type to set.

Source

Thrown at packages/http-crawler/src/internals/utils.ts:29

/**
 * Converts {@apilink HttpRequestOptions} to a {@apilink HttpRequest}.
 */
export function processHttpRequestOptions({
    searchParams,
    form,
    json,
    username,
    password,
    ...request
}: HttpRequestOptions): HttpRequest {
    const url = new URL(request.url);
    const headers = new Headers(request.headers);

    applySearchParams(url, searchParams);

    if ([request.body, form, json].filter((value) => value !== undefined).length > 1) {
        throw new Error('At most one of `body`, `form` and `json` may be specified in sendRequest arguments');
    }

    const body = (() => {
        if (form !== undefined) {
            return Readable.from(new URLSearchParams(form).toString());
        }

        if (json !== undefined) {
            return Readable.from(JSON.stringify(json));
        }

        if (request.body !== undefined) {
            return Readable.from(request.body);
        }

        return undefined;
    })();

View on GitHub (pinned to dbe57fb09c)

Solutions

  1. Keep exactly one payload option: delete or set to `undefined` the others before calling.
  2. If you need a custom payload, put it in `body` (as string/Buffer/stream) and set the Content-Type header yourself instead of combining with `form`/`json`.
  3. Check merged/spread option objects so a previous `json`/`form` is not retained alongside `body`.
  4. Use `null`-safe checks in your own code — note the library only skips `undefined`, so pass `undefined`, not `null`, for absent options.

Example fix

// before
await crawler.sendRequest({ url, json: { a: 1 }, body: 'raw-data' });

// after
await crawler.sendRequest({ url, json: { a: 1 } });
// or for raw payloads:
await crawler.sendRequest({ url, body: 'raw-data', headers: { 'content-type': 'text/plain' } });
Defensive patterns

Strategy: validation

Validate before calling

const payloadKeys = ['body', 'form', 'json'].filter((k) => opts[k] !== undefined);
if (payloadKeys.length > 1) {
    throw new Error(`Only one of body/form/json allowed, got: ${payloadKeys.join(', ')}`);
}

Type guard

function hasSinglePayload(o: { body?: unknown; form?: unknown; json?: unknown }): boolean {
    return ['body', 'form', 'json'].filter((k) => o[k as keyof typeof o] !== undefined).length <= 1;
}

Try / catch

try {
    await crawler.sendRequest(opts);
} catch (err) {
    if ((err as Error).message.includes('At most one of `body`')) {
        logger.error('Conflicting payload options', { opts });
    }
    throw err;
}

Prevention

When it happens

Trigger: Calling sendRequest / HttpCrawler / crawler.run with e.g. both `json` and `body` set, both `form` and `json` set, or a request object that already carries `body` while the caller also passes `json` or `form`. Values of `null` count as defined — only `undefined` is treated as absent.

Common situations: Migrating code that sets a default JSON payload and then adds a raw body override; building request options dynamically and forgetting to clear a previously assigned `form`; spreading merged option objects where two payload keys survive; assuming `null` means 'not set'.

Related errors


AI-assisted analysis of apify/crawlee@dbe57fb09c (2026-08-30). Data as JSON: /api/errors/47f9e6ed9ea82313. Report an issue: GitHub.