Skip to content

challengeFactor() (and any all-optional-body endpoint) sends [] instead of {}, rejected by API with 422 #400

@tjmartin69

Description

@tjmartin69

This has only started failing today and no code changes have been made to the SDK. My guess is a changed was made on your APIs to reject this.

Affected versions: confirmed in v6.0.2 and v7.1.0 (latest).
Environment: PHP 8.5, guzzlehttp/guzzle 7.x

Summary

MultiFactorAuth::challengeFactor() builds its request body with array_filter() over an all-optional payload. When the only field (sms_template) is omitted — the normal case for a TOTP factor — the body is an empty PHP array []. The SDK's HttpClient then passes it to Guzzle as 'json' => [], which json_encodes to the JSON array [] rather than an object {}. The WorkOS API rejects the non-object body with a 422 Unprocessable Entity (validation: "an unknown value was passed to the validate function").

Reproduction

$workos = new WorkOS\WorkOS(apiKey: $key, clientId: $clientId);
// TOTP factor → no sms_template
$workos->multiFactorAuth()->challengeFactor('auth_factor_...');
// → 422; request body on the wire is `[]`, not `{}`

Root cause

lib/Service/MultiFactorAuth.php (challengeFactor):

$body = array_filter(['sms_template' => $smsTemplate], fn ($v) => $v !== null); // [] when omitted
$this->client->request(method: 'POST', path: 'auth/factors/.../challenge', body: $body, ...);

lib/HttpClient.php (buildRequestOptions):

if ($body !== null) {
    $requestOptions['json'] = $body; // [] → Guzzle encodes to `[]`
}

Guzzle's json option does not force object encoding, so an empty array serializes as []. This is a general problem for any endpoint whose body is entirely optional and fully omitted, not just challengeFactor.

Expected

The request body for a JSON-object endpoint should serialize as {} (or the endpoint should send no body), so the API accepts the call.

Suggested fix (any one)

  1. In HttpClient, when a JSON body is an empty array, encode it as an object — e.g. send 'body' => '{}' with the JSON content-type, or json_encode($body, JSON_FORCE_OBJECT) for associative/empty payloads.
  2. In endpoints with all-optional bodies, cast to object: body: (object) $body.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions