powertools-lambda-typescript icon indicating copy to clipboard operation
powertools-lambda-typescript copied to clipboard

Bug: LambdaFunctionUrlEnvelope assumes body is always a JSON string

Open dreamorosi opened this issue 1 year ago • 0 comments

Expected Behavior

When using Parser with Lambda Function URL I should be able to parse requests that have a valid body that is not encoded as a JSON string. For example, when the body is a plain string or when it's a binary (& the isBase64Encoded field is true).

Current Behavior

Unless the body field is a JSON string, the parsing fails.

Code snippet

import { Logger, LogLevel } from '@aws-lambda-powertools/logger';
import { parser } from '@aws-lambda-powertools/parser/middleware';
import { LambdaFunctionUrlEnvelope } from '@aws-lambda-powertools/parser/envelopes';
import type { ParsedResult } from '@aws-lambda-powertools/parser/types';
import middy from '@middy/core';
import { z } from 'zod';

const logger = new Logger({ logLevel: LogLevel.DEBUG });

export const handler = middy(async (event: ParsedResult) => {
  logger.logEventIfEnabled(event);

  return {
    statusCode: 200,
    body: JSON.stringify('Hello, World!'),
  };
}).use(
  parser({
    schema: z.string(),
    envelope: LambdaFunctionUrlEnvelope,
    safeParse: true,
  })
);

Steps to Reproduce

For binary requests:

import {
  type BinaryLike,
  type KeyObject,
  createHash,
  createHmac,
} from 'node:crypto';
import { URL } from 'node:url';
import { HttpRequest } from '@smithy/protocol-http';
import { SignatureV4 } from '@smithy/signature-v4';

class Sha256 {
  private readonly hash;

  public constructor(secret?: unknown) {
    this.hash = secret
      ? createHmac('sha256', secret as BinaryLike | KeyObject)
      : createHash('sha256');
  }

  public digest(): Promise<Uint8Array> {
    const buffer = this.hash.digest();

    return Promise.resolve(new Uint8Array(buffer.buffer));
  }

  public update(array: Uint8Array): void {
    this.hash.update(array);
  }
}

const signer = new SignatureV4({
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? '',
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? '',
    sessionToken: process.env.AWS_SESSION_TOKEN ?? '',
  },
  service: 'lambda',
  region: process.env.AWS_REGION ?? 'eu-west-1',
  sha256: Sha256,
});

const buildHttpRequest = (apiUrl: string): HttpRequest => {
  const url = new URL(apiUrl);

  const buffer = Buffer.alloc(10);

  return new HttpRequest({
    hostname: url.hostname,
    path: url.pathname,
    body: buffer,
    method: 'POST',
    headers: {
      host: url.hostname,
    },
  });
};

const sendAuthrequest = async (apiUrl: string): Promise<void> => {
  // Build the HTTP request to be signed
  const httpRequest = buildHttpRequest(apiUrl);
  // Sign the request
  const signedHttpRequest = await signer.sign(httpRequest);
  try {
    // Send the request
    const result = await fetch(apiUrl, {
      headers: new Headers(signedHttpRequest.headers),
      body: signedHttpRequest.body,
      method: signedHttpRequest.method,
    });

    if (!result.ok) throw new Error(result.statusText);

    const body = await result.json();

    console.log('Response:', body);
  } catch (err) {
    console.error(err as Error);
    throw new Error('Failed to send request', { cause: err });
  }
};

await sendAuthrequest(
  'https://<api-id>.lambda-url.eu-west-1.on.aws/'
);

For raw string requests, use the same code as above, but change the body in the buildHttpRequest function to just "hello world" or any string.

Possible Solution

No response

Powertools for AWS Lambda (TypeScript) version

latest

AWS Lambda function runtime

20.x

Packaging format used

npm

Execution logs

{
    "success": false,
    "error": {
        "name": "ParseError",
        "location": "file:///var/task/index.mjs:3",
        "message": "Failed to parse Lambda function URL body. This error was caused by: Failed to parse envelope. This error was caused by: Unexpected token 'A', \"AAAAAAAAAAAAAA==\" is not valid JSON..",
        "stack": "ParseError: Failed to parse Lambda function URL body. This error was caused by: Failed to parse envelope. This error was caused by: Unexpected token 'A', \"AAAAAAAAAAAAAA==\" is not valid JSON..\n    at Object.safeParse (file:///var/task/index.mjs:3:82990)\n    at _r (file:///var/task/index.mjs:3:9998)\n    at before (file:///var/task/index.mjs:3:10351)\n    at Vt (file:///var/task/index.mjs:3:85173)\n    at co (file:///var/task/index.mjs:3:84503)\n    at Runtime.i [as handler] (file:///var/task/index.mjs:3:83598)\n    at Runtime.handleOnceNonStreaming (file:///var/runtime/index.mjs:1173:29)",
        "cause": {
            "name": "ParseError",
            "location": "file:///var/task/index.mjs:3",
            "message": "Failed to parse envelope. This error was caused by: Unexpected token 'A', \"AAAAAAAAAAAAAA==\" is not valid JSON.",
            "stack": "ParseError: Failed to parse envelope. This error was caused by: Unexpected token 'A', \"AAAAAAAAAAAAAA==\" is not valid JSON.\n    at Object.safeParse (file:///var/task/index.mjs:3:70629)\n    at Object.safeParse (file:///var/task/index.mjs:3:82928)\n    at _r (file:///var/task/index.mjs:3:9998)\n    at before (file:///var/task/index.mjs:3:10351)\n    at Vt (file:///var/task/index.mjs:3:85173)\n    at co (file:///var/task/index.mjs:3:84503)\n    at Runtime.i [as handler] (file:///var/task/index.mjs:3:83598)\n    at Runtime.handleOnceNonStreaming (file:///var/runtime/index.mjs:1173:29)",
            "cause": {
                "name": "SyntaxError",
                "location": "file:///var/task/index.mjs:3",
                "message": "Unexpected token 'A', \"AAAAAAAAAAAAAA==\" is not valid JSON",
                "stack": "SyntaxError: Unexpected token 'A', \"AAAAAAAAAAAAAA==\" is not valid JSON\n    at JSON.parse (<anonymous>)\n    at Object.safeParse (file:///var/task/index.mjs:3:70457)\n    at Object.safeParse (file:///var/task/index.mjs:3:82928)\n    at _r (file:///var/task/index.mjs:3:9998)\n    at before (file:///var/task/index.mjs:3:10351)\n    at Vt (file:///var/task/index.mjs:3:85173)\n    at co (file:///var/task/index.mjs:3:84503)\n    at Runtime.i [as handler] (file:///var/task/index.mjs:3:83598)\n    at Runtime.handleOnceNonStreaming (file:///var/runtime/index.mjs:1173:29)"
            }
        }
    },
    "originalEvent": {
        "version": "2.0",
        "routeKey": "$default",
        "rawPath": "/",
        "rawQueryString": "",
        "headers": {
            "sec-fetch-mode": "cors",
            "x-amz-content-sha256": "01d448afd928065458cf670b60f5a594d735af0172c8d67f22a81680132681ca",
            "content-length": "10",
            "x-amzn-tls-version": "TLSv1.3",
            "accept-language": "*",
            "x-amz-date": "20241014T100603Z",
            "x-forwarded-proto": "https",
            "x-forwarded-port": "443",
            "x-forwarded-for": "92.177.95.206",
            "x-amz-security-token": "....",
            "accept": "*/*",
            "x-amzn-tls-cipher-suite": "TLS_AES_128_GCM_SHA256",
            "x-amzn-trace-id": "Root=1-670ced0b-7334ddc02966931874eeca3c",
            "host": "api-id.lambda-url.eu-west-1.on.aws",
            "accept-encoding": "br, gzip, deflate",
            "user-agent": "node"
        },
        "requestContext": {
            "accountId": "123456789023",
            "apiId": "api-id",
            "authorizer": {
                "iam": {
                    "accessKey": "ASIA...............Z",
                    "accountId": "123456789023",
                    "callerId": "AROA...............Z:aamorosi-role",
                    "cognitoIdentity": null,
                    "principalOrgId": null,
                    "userArn": "arn:aws:sts::123456789023:assumed-role/Admin/aamorosi-role",
                    "userId": "AROA...............Z:aamorosi-role"
                }
            },
            "domainName": "api-id.lambda-url.eu-west-1.on.aws",
            "domainPrefix": "api-id",
            "http": {
                "method": "POST",
                "path": "/",
                "protocol": "HTTP/1.1",
                "sourceIp": "92.177.95.206",
                "userAgent": "node"
            },
            "requestId": "1a2de596-d389-469e-ba8f-6d0b080b8297",
            "routeKey": "$default",
            "stage": "$default",
            "time": "14/Oct/2024:10:06:04 +0000",
            "timeEpoch": 1728900364011
        },
        "body": "AAAAAAAAAAAAAA==",
        "isBase64Encoded": true
    }
}

dreamorosi avatar Oct 14 '24 10:10 dreamorosi