Skip to content

Bug: APIGatewayRestResolver(enable_validation=True) is validating middleware responses #5228

Open
@ob1-dev

Description

@ob1-dev

Expected Behaviour

Given the documentation, I would have expected the Validation to be skipped in the situation a middleware responds before the request gets to the route handler.

Current Behaviour

Current behaviour is that my error handler captures a the raise in the OpenAPI validation

{
  "level": "ERROR",
  "location": "handle_error_boundary:51",
  "message": "Error in GET /test: ",
  "timestamp": "2024-09-24 13:24:05,393+0000",
  "service": "test",
  "sampling_rate": "0.1",
  "cold_start": true,
  "function_name": "ApiFunction",
  "function_memory_size": "128",
  "function_arn": "arn:aws:lambda:us-east-1:012345678912:function:ApiFunction",
  "function_request_id": "e67d066f-c78e-4367-88cd-2bc6e9c48894",
  "correlation_id": "07c501c3-9b3b-45fd-aaba-d5c6598f8b7c",
  "path": "/test",
  "query_strings": {},
  "exec_info": "",
  "exception": "Traceback (most recent call last):\n  File \"/opt/python/aws_lambda_powertools/event_handler/api_gateway.py\", line 2277, in _call_route\n    route(router_middlewares=self._router_middlewares, app=self, route_arguments=route_arguments),\n    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/python/aws_lambda_powertools/event_handler/api_gateway.py\", line 432, in __call__\n    return self._middleware_stack(app)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/python/aws_lambda_powertools/event_handler/api_gateway.py\", line 1409, in __call__\n    return self.current_middleware(app, self.next_middleware)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/python/aws_lambda_powertools/event_handler/middlewares/base.py\", line 121, in __call__\n    return self.handler(app, next_middleware)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/python/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py\", line 136, in handler\n    return self._handle_response(route=route, response=response)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/python/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py\", line 143, in _handle_response\n    response.body = self._serialize_response(\n                    ^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/python/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py\", line 178, in _serialize_response\n    raise RequestValidationError(errors=_normalize_errors(errors), body=response_content)\naws_lambda_powertools.event_handler.openapi.exceptions.RequestValidationError",
  "exception_name": "RequestValidationError",
  "stack_trace": {
    "type": "RequestValidationError",
    "value": "",
    "module": "aws_lambda_powertools.event_handler.openapi.exceptions",
    "frames": [
      {
        "file": "/opt/python/aws_lambda_powertools/event_handler/api_gateway.py",
        "line": 2277,
        "function": "_call_route",
        "statement": "route(router_middlewares=self._router_middlewares, app=self, route_arguments=route_arguments),"
      },
      {
        "file": "/opt/python/aws_lambda_powertools/event_handler/api_gateway.py",
        "line": 432,
        "function": "__call__",
        "statement": "return self._middleware_stack(app)"
      },
      {
        "file": "/opt/python/aws_lambda_powertools/event_handler/api_gateway.py",
        "line": 1409,
        "function": "__call__",
        "statement": "return self.current_middleware(app, self.next_middleware)"
      },
      {
        "file": "/opt/python/aws_lambda_powertools/event_handler/middlewares/base.py",
        "line": 121,
        "function": "__call__",
        "statement": "return self.handler(app, next_middleware)"
      },
      {
        "file": "/opt/python/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py",
        "line": 136,
        "function": "handler",
        "statement": "return self._handle_response(route=route, response=response)"
      },
      {
        "file": "/opt/python/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py",
        "line": 143,
        "function": "_handle_response",
        "statement": "response.body = self._serialize_response("
      },
      {
        "file": "/opt/python/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py",
        "line": 178,
        "function": "_serialize_response",
        "statement": "raise RequestValidationError(errors=_normalize_errors(errors), body=response_content)"
      }
    ]
  }
}

Code snippet

from aws_lambda_powertools.event_handler import (
    APIGatewayRestResolver,
    Response
)
from pydantic import BaseModel, ConfigDict
from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.event_handler.openapi.exceptions import (
    RequestValidationError,
)
from aws_lambda_powertools.event_handler.middlewares import NextMiddleware

import json


###
#   Test middleware
###
def test_middleware(app: APIGatewayRestResolver, next_middleware: NextMiddleware):
    # return next_middleware(app)
    return Response(status_code=401,content_type="application/json", body="{}")

def test_middleware2(app: APIGatewayRestResolver, next_middleware: NextMiddleware):
    resp = next_middleware(app)
    print(resp)
    return resp
    

###
#   Test Pydantic Validation
###
class TestModel(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    name: str
    age: int


logger = Logger(service="test")
tracer = Tracer(service="test")

app = APIGatewayRestResolver(enable_validation=True)
app.use(middlewares=[test_middleware2, test_middleware])

@app.exception_handler(Exception)
def handle_error_boundary(ex: Exception):
    metadata = {
        "path": app.current_event.path,
        "query_strings": app.current_event.query_string_parameters,
    }
    logger.exception(
        f"Error in {app.current_event.http_method} {app.current_event.path}: {ex}",
        exec_info=ex,
        extra=metadata,
    )
    return Response(
        status_code=500,
        content_type="application/json",
        body=json.dumps({"error": "Internal server error"}),
    )

@app.get("/test")
@tracer.capture_method
def test() -> TestModel:
    return {"name": "Chris", "age": 30}

@logger.inject_lambda_context(
    correlation_id_path=correlation_paths.API_GATEWAY_REST, log_event=True
)
@tracer.capture_lambda_handler
def handler(event, context):
    return app.resolve(event, context)

Possible Solution

Perhaps the support of the Union of responses from an endpoint, or in the event a request doesn't make it to the route that Validation isn't performed.

Steps to Reproduce

template.yml

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Hello world event handler API Gateway

Globals:
  Api:
    TracingEnabled: true
    BinaryMediaTypes: # see Binary responses section
      - "*~1*" # converts to */* for any binary type
      # NOTE: use this stricter version if you're also using CORS; */* doesn't work with CORS
      # see: https://github.com/aws-powertools/powertools-lambda-python/issues/3373#issuecomment-1821144779
      # - "image~1*" # converts to image/*
      # - "*~1csv" # converts to */csv, eg text/csv, application/csv

  Function:
    Timeout: 5
    Runtime: python3.12
    Tracing: Active
    Environment:
      Variables:
        POWERTOOLS_LOG_LEVEL: DEBUG
        POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
        POWERTOOLS_LOGGER_LOG_EVENT: true
        POWERTOOLS_SERVICE_NAME: example

Resources:
  MyLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: MyLayer
      ContentUri: .
      CompatibleRuntimes:
        - python3.12
    Metadata:
      BuildMethod: python3.12
  ApiFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: lambda_test.handler
      CodeUri: src
      Description: API handler function
      Layers:
        - !Ref MyLayer
      Events:
        AnyApiEvent:
          Type: Api
          Properties:
            # NOTE: this is a catch-all rule to simplify the documentation.
            # explicit routes and methods are recommended for prod instead (see below)
            Path: /{proxy+} # Send requests on any path to the lambda function
            Method: ANY # Send requests using any http method to the lambda function

pyproject.toml

[tool.poetry]
name = "powertools-demo"
version = "0.1.0"
description = ""
authors = ["ob1-dev <chris+github@ob1.nz>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.12"
aws-lambda-powertools = {extras = ["pydantic", "tracer"], version = "^3.0.0"}
pydantic = "^2.9.2"


[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

Preparing the requirements.txt

poetry export -f requirements.txt --without-hashes --output requirements.txt

SAM Build and Local Start-API

sam build; sam local start-api

Browse to localhost

Hitting specifically http://localhost:3000/test will now trigger the validation error

Powertools for AWS Lambda (Python) version

latest

AWS Lambda function runtime

3.12

Packaging format used

Lambda Layers

Debugging logs

START RequestId: 94462763-631d-45a9-87e1-2ae225a7f317 Version: $LATEST
{"level":"INFO","location":"decorate:445","message":{"body":null,"headers":{"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7","Accept-Encoding":"gzip, deflate, br, zstd","Accept-Language":"en-US,en;q=0.9","Cache-Control":"max-age=0","Connection":"keep-alive","Dnt":"1","Host":"127.0.0.1:3000","Sec-Ch-Ua":"\"Chromium\";v=\"129\", \"Not=A?Brand\";v=\"8\"","Sec-Ch-Ua-Mobile":"?0","Sec-Ch-Ua-Platform":"\"macOS\"","Sec-Fetch-Dest":"document","Sec-Fetch-Mode":"navigate","Sec-Fetch-Site":"none","Sec-Fetch-User":"?1","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36","X-Forwarded-Port":"3000","X-Forwarded-Proto":"http"},"httpMethod":"GET","isBase64Encoded":true,"multiValueHeaders":{"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Accept-Language":["en-US,en;q=0.9"],"Cache-Control":["max-age=0"],"Connection":["keep-alive"],"Dnt":["1"],"Host":["127.0.0.1:3000"],"Sec-Ch-Ua":["\"Chromium\";v=\"129\", \"Not=A?Brand\";v=\"8\""],"Sec-Ch-Ua-Mobile":["?0"],"Sec-Ch-Ua-Platform":["\"macOS\""],"Sec-Fetch-Dest":["document"],"Sec-Fetch-Mode":["navigate"],"Sec-Fetch-Site":["none"],"Sec-Fetch-User":["?1"],"Upgrade-Insecure-Requests":["1"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"],"X-Forwarded-Port":["3000"],"X-Forwarded-Proto":["http"]},"multiValueQueryStringParameters":null,"path":"/test","pathParameters":{"proxy":"test"},"queryStringParameters":null,"requestContext":{"accountId":"123456789012","apiId":"1234567890","domainName":"127.0.0.1:3000","extendedRequestId":null,"httpMethod":"GET","identity":{"accountId":null,"apiKey":null,"caller":null,"cognitoAuthenticationProvider":null,"cognitoAuthenticationType":null,"cognitoIdentityPoolId":null,"sourceIp":"127.0.0.1","user":null,"userAgent":"Custom User Agent String","userArn":null},"path":"/{proxy+}","protocol":"HTTP/1.1","requestId":"37fea8d1-981b-435e-9c95-37e0ce86c7f6","requestTime":"24/Sep/2024:13:33:27 +0000","requestTimeEpoch":1727184807,"resourceId":"123456","resourcePath":"/{proxy+}","stage":"Prod"},"resource":"/{proxy+}","stageVariables":null},"timestamp":"2024-09-24 13:33:39,740+0000","service":"test","sampling_rate":"0.1","cold_start":true,"function_name":"ApiFunction","function_memory_size":"128","function_arn":"arn:aws:lambda:us-east-1:012345678912:function:ApiFunction","function_request_id":"bd54493d-19af-4d67-9d52-b3fb859bd5f2","correlation_id":"37fea8d1-981b-435e-9c95-37e0ce86c7f6"}
<aws_lambda_powertools.event_handler.api_gateway.Response object at 0x7ffffdb173b0>
{"level":"ERROR","location":"handle_error_boundary:51","message":"Error in GET /test: ","timestamp":"2024-09-24 13:33:39,745+0000","service":"test","sampling_rate":"0.1","cold_start":true,"function_name":"ApiFunction","function_memory_size":"128","function_arn":"arn:aws:lambda:us-east-1:012345678912:function:ApiFunction","function_request_id":"bd54493d-19af-4d67-9d52-b3fb859bd5f2","correlation_id":"37fea8d1-981b-435e-9c95-37e0ce86c7f6","path":"/test","query_strings":{},"exec_info":"","exception":"Traceback (most recent call last):\n  File \"/opt/python/aws_lambda_powertools/event_handler/api_gateway.py\", line 2277, in _call_route\n    route(router_middlewares=self._router_middlewares, app=self, route_arguments=route_arguments),\n    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/python/aws_lambda_powertools/event_handler/api_gateway.py\", line 432, in __call__\n    return self._middleware_stack(app)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/python/aws_lambda_powertools/event_handler/api_gateway.py\", line 1409, in __call__\n    return self.current_middleware(app, self.next_middleware)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/python/aws_lambda_powertools/event_handler/middlewares/base.py\", line 121, in __call__\n    return self.handler(app, next_middleware)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/python/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py\", line 136, in handler\n    return self._handle_response(route=route, response=response)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/python/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py\", line 143, in _handle_response\n    response.body = self._serialize_response(\n                    ^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/opt/python/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py\", line 178, in _serialize_response\n    raise RequestValidationError(errors=_normalize_errors(errors), body=response_content)\naws_lambda_powertools.event_handler.openapi.exceptions.RequestValidationError","exception_name":"RequestValidationError","stack_trace":{"type":"RequestValidationError","value":"","module":"aws_lambda_powertools.event_handler.openapi.exceptions","frames":[{"file":"/opt/python/aws_lambda_powertools/event_handler/api_gateway.py","line":2277,"function":"_call_route","statement":"route(router_middlewares=self._router_middlewares, app=self, route_arguments=route_arguments),"},{"file":"/opt/python/aws_lambda_powertools/event_handler/api_gateway.py","line":432,"function":"__call__","statement":"return self._middleware_stack(app)"},{"file":"/opt/python/aws_lambda_powertools/event_handler/api_gateway.py","line":1409,"function":"__call__","statement":"return self.current_middleware(app, self.next_middleware)"},{"file":"/opt/python/aws_lambda_powertools/event_handler/middlewares/base.py","line":121,"function":"__call__","statement":"return self.handler(app, next_middleware)"},{"file":"/opt/python/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py","line":136,"function":"handler","statement":"return self._handle_response(route=route, response=response)"},{"file":"/opt/python/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py","line":143,"function":"_handle_response","statement":"response.body = self._serialize_response("},{"file":"/opt/python/aws_lambda_powertools/event_handler/middlewares/openapi_validation.py","line":178,"function":"_serialize_response","statement":"raise RequestValidationError(errors=_normalize_errors(errors), body=response_content)"}]}}
END RequestId: bd54493d-19af-4d67-9d52-b3fb859bd5f2

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingon-holdThis item is on-hold and will be revisited in the future

    Type

    No type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions