Skip to content

Deserialization fails on JSON numbers in exponent notation (regression in @aws-sdk/core 3.977.0) — breaks all ecs:DescribeServices calls #8224

Description

@MohMaya

Checkboxes for prior research

Describe the bug

Response deserialization throws whenever an awsJson response body contains a JSON number written in exponent notation (e.g. 1.784316439424E9). Exponent notation is valid JSON per RFC 8259, and AWS services do emit it — ECS serializes epoch-second timestamps that way, so every ecs:DescribeServices call now fails.

Error: @smithy/core/serde - NumericValue must only contain [0-9], at most one decimal point ".", and an optional negation prefix "-".
  Deserialization error: to see the raw response, inspect the hidden field {error}.$response on this object.

The same responses parse correctly with botocore / the AWS CLI, and parsed correctly with the JS SDK before this regression.

Regression Issue

  • Select this option if this issue appears to be a regression.

Last good: @aws-sdk/core@3.976.0
First bad: @aws-sdk/core@3.977.0 (published 2026-07-24, shipped with v3.1095.0)

Suspect: #8214 (chore(core/protocols): JSON serde consistency testing and perf tuning), which touches parseJsonBody.ts, needsReviver.ts, and JsonShapeDeserializer.ts.

Note this floats independently of the service client — pinning @aws-sdk/client-* or @smithy/core does not avoid it, because @aws-sdk/core is resolved by range.

SDK version number

@aws-sdk/core@3.977.0, reproduced with @aws-sdk/client-ecs@3.1094.0 and 3.1095.0 (any client using the awsJson protocol should reproduce)

Which JavaScript Runtime is this issue in?

Node.js

Details of the browser/Node.js/ReactNative version

Node.js v24.12.0 (Linux and macOS). Requires a runtime with JSON.parse source access (Node ≥ 22) — on older Node the reviver's context is undefined and the bug does not trigger.

Reproduction Steps

Fully self-contained: no AWS account or credentials needed, since the HTTP layer is stubbed with a canned response body.

mkdir repro && cd repro
npm init -y >/dev/null
npm pkg set type=module
npm install @aws-sdk/client-ecs@3.1094.0 @smithy/protocol-http

repro.mjs:

import { ECS } from "@aws-sdk/client-ecs";
import { HttpResponse } from "@smithy/protocol-http";
import { Readable } from "node:stream";

// A minimal awsJson response. `createdAt` uses exponent notation, exactly as
// real AWS services serialize epoch-second timestamps.
const BODY =
  '{"failures":[],"services":[{"serviceName":"my-service","status":"ACTIVE","createdAt":1.784316439424E9}]}';

const client = new ECS({
  region: "us-east-1",
  credentials: { accessKeyId: "AKIAIOSFODNN7EXAMPLE", secretAccessKey: "secret" },
  requestHandler: {
    async handle() {
      return {
        response: new HttpResponse({
          statusCode: 200,
          headers: { "content-type": "application/x-amz-json-1.1" },
          body: Readable.from([BODY]),
        }),
      };
    },
  },
});

try {
  const out = await client.describeServices({ cluster: "my-cluster", services: ["my-service"] });
  console.log("OK ->", JSON.stringify(out.services));
} catch (e) {
  console.log("FAIL ->", e.message.split("\n")[0]);
}
node repro.mjs
# FAIL -> @smithy/core/serde - NumericValue must only contain [0-9], at most one decimal point ".", ...

# Now pin the previous version of @aws-sdk/core and repeat:
npm pkg set overrides.@aws-sdk/core=3.976.0 && npm install
node repro.mjs
# OK -> [{"serviceName":"my-service","status":"ACTIVE","createdAt":"2026-07-17T19:27:19.424Z"}]

Against the live API, ecs:DescribeServices reproduces it for any service, because ECS returns createdAt / updatedAt in exponent form.

Observed Behavior

describeServices (and any awsJson call whose response contains an exponent-notation number) rejects with the NumericValue error above. Nothing is returned to the caller, and the error text names neither the field nor the offending value.

Expected Behavior

The response deserializes, with createdAt becoming a Date (as it does on @aws-sdk/core@3.976.0). Exponent notation is valid JSON and should be accepted wherever a plain decimal is.

Possible Solution

The reviver in @aws-sdk/core (submodules/protocols/index.js, jsonReviver) decides a token is precision-losing by comparing the raw source text against String(value):

function jsonReviver(key, value, context) {
  if (context?.source) {
    const numericString = context.source;
    if (typeof value === "number") {
      if (value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER || numericString !== String(value)) {
        const isFractional = numericString.includes(".");
        if (isFractional) {
          return new NumericValue(numericString, "bigDecimal");  // throws on "1.78E9"
        } else {
          return BigInt(numericString);                          // throws on "1E9"
        }
      }
    }
  }
  return value;
}

An exponent-notation token always fails numericString !== String(value) ("1.784316439424E9" !== "1784316439.424"), so it is misclassified as precision-losing even though the double round-trips exactly. Both branches then reject it:

  • fractional → NumericValue, whose format is /^-?\d*(\.\d+)?$/ and forbids E
  • non-fractional → BigInt("1E9") throws SyntaxError: Cannot convert 1E9 to a BigInt

Suggested fixes, roughly in order of smallest blast radius:

  1. Normalize the exponent away before the comparison / before constructing NumericValue, so exponent form is expanded to plain decimal.
  2. Treat a token as precision-losing only when it genuinely does not round-trip — compare numerically rather than by string equality (e.g. against Number(numericString)), which lets 1.78E9 fall through to the plain number path.
  3. Widen the NumericValue format in @smithy/core to accept the full JSON number grammar, including exponents, and make the non-fractional branch use a parser that accepts them.

Whichever direction, a regression test with exponent-notation input on both branches (1.5E9 and 1E9) would pin it.

Additional Information/Context

Impact is broad and not opt-in: any code path that reads an ECS service description with the JS SDK fails, which includes common CI/CD deployment tooling built on the SDK. The failure surfaces as an opaque error with no field name, and — because tools often capture the SDK's stdout — can present as an exit code with no message at all, which makes it expensive to diagnose.

botocore and the AWS CLI parse the identical response bodies without complaint, so this is specific to the JS SDK's reviver.

Pinned by kuhe

Metadata

Metadata

Assignees

Labels

bugThis issue is a bug.p1This is a high priority issuepotential-regressionMarking this issue as a potential regression to be checked by team memberreleasedThe change has been released and there is a comment with the release version.response-requestedWaiting on additional info and feedback. Will move to \"closing-soon\" in 7 days.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions