Skip to content

Bug: routing and Response issues: unescaped route regex, regex strip_prefixes, gzip q=0, bytes JSON body, is_json() casing, missing Vary: Origin #8580

Open

Description

@leandrodamascena

Expected Behaviour

  • Literal route segments match literally, and a regex strip_prefixes only strips the prefix once at the start of the path.
  • Response handles a bytes body with a JSON content type, finds the content type header case-insensitively, and only compresses when the client accepts gzip.
  • CORS responses that echo a specific origin carry Vary: Origin.

Current Behaviour

Six routing and response bugs in api_gateway.py, the first and third confirmed through a real API Gateway REST API and the last through a real HTTP API.

  1. Rules compiled as unescaped regex (_compile_regex, line 2778). /items.json matches /itemsXjson (200). A literal /a+b returns 404 for /a+b. /a|b matches b.
  2. Regex strip_prefixes replaces every occurrence (line 2809). re.sub(prefix, "", path) has no count=1 and no anchor: strip_prefixes=[re.compile(r"/v[1-9]+")] on /v1/users/v2 yields /users, 404.
  3. Accept-Encoding: gzip;q=0 still compresses, GZIP does not (line 755). The check is a substring test. Real API: content-encoding: gzip returned to a client that refused gzip.
  4. Response(content_type=APPLICATION_JSON, body=b'...') raises TypeError (line 838) when enable_validation=False. The docstring says bytes are base64 encoded.
  5. is_json() only matches Content-Type with exact casing (line 364). Response(headers={"content-type": "application/json"}, body={...}) returns the dict unserialized. Real API: 502.
  6. No Vary: Origin (CORSConfig.to_dict, line 232). With allow_origin="https://a.com", extra_origins=["https://b.com"] the response echoes https://a.com with no Vary, so a shared cache can serve it to origin B.

Code snippet

import re
from aws_lambda_powertools.event_handler import APIGatewayRestResolver, ALBResolver, CORSConfig, Response, content_types
from tests.functional.utils import load_event

ev = load_event("apiGatewayProxyEvent.json")

def get(rule, path, **kw):
    app = APIGatewayRestResolver(**kw)
    @app.get(rule)
    def r(**args): return {"args": args}
    e = dict(ev); e["path"] = path
    return app.resolve(e, None)["statusCode"]

print(get("/items.json", "/itemsXjson"))                                        # 200 (expected 404)
print(get("/a+b", "/a+b"))                                                      # 404 (expected 200)
print(get("/users/<name>", "/v1/users/v2", strip_prefixes=[re.compile(r"/v[1-9]+")]))  # 404 (expected 200)

app = APIGatewayRestResolver()
@app.get("/my/path", compress=True)
def gz(): return {"a": "b" * 50}
e = dict(ev); e["headers"] = {"Accept-Encoding": "gzip;q=0"}; e["multiValueHeaders"] = {"Accept-Encoding": ["gzip;q=0"]}
print(app.resolve(e, None)["multiValueHeaders"].get("Content-Encoding"))      # ['gzip'] (expected None)

app = APIGatewayRestResolver()
@app.get("/my/path")
def by(): return Response(200, content_types.APPLICATION_JSON, body=b'{"a":1}')
app.resolve(ev, None)                                                           # TypeError: bytes is not JSON serializable

app = APIGatewayRestResolver()
@app.get("/my/path")
def lower(): return Response(200, headers={"content-type": "application/json"}, body={"a": 1})
print(type(app.resolve(ev, None)["body"]))                                      # <class 'dict'>

alb = ALBResolver(cors=CORSConfig(allow_origin="https://a.com", extra_origins=["https://b.com"]))
@alb.get("/lambda")
def c(): return {}
e = load_event("albEvent.json"); e["headers"]["origin"] = "https://a.com"
print(alb.resolve(e, None)["headers"])                                          # no Vary

Possible Solution

re.escape the literal fragments in _compile_regex; re.sub(..., count=1) anchored at the start; parse Accept-Encoding tokens with q values and lower-case them; in ResponseBuilder.build skip serialization for bytes; case-insensitive header lookup in is_json(); add Vary: Origin when the echoed origin is not *.

Steps to Reproduce

Run the snippet above with the tests/events fixtures. Full context in #8576.

Powertools for AWS Lambda (Python) version

latest

AWS Lambda function runtime

3.13

Packaging format used

PyPi

Debugging logs

Warning

This issue is ready to be picked up, by humans or by agents. If you want it, comment here first so we can assign it to you, and please take only one or two issues at a time so others can join in too. Thanks!

Activity

  1. added this to the 3.38.0 milestone on Oct 18, 2026
  2. Oakcube4life commented on Oct 18, 2026

    @Oakcube4life

    Hi! I’d like to take this one. I’m planning to fix all six items in api_gateway.py as described, with a functional test for each. Could you assign it to me?

  3. leandrodamascena commented on Oct 19, 2026

    @leandrodamascena
    ContributorAuthor

    Hey @Oakcube4life pls go ahead. It's six small fixes, feel free to split into two PRs if it gets big.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions