pythonamazon-web-servicesaws-lambdafastapiredirect-loop

FastAPI app works locally, but /blogs endpoint causes redirect loop on AWS Lambda deployment


I’ve developed a FastAPI app and deployed it on AWS Lambda as a .zip archive. Locally, when I run:

uvicorn src.main:app --reload

I can access the /blogs endpoint (http://127.0.0.1:8000/blogs) and it returns the correct data:

[
  {
    "title": "blog with relationship",
    "description": "description",
    "written_by": {
      "username": "Sarthak",
      "email": "sss@ssss.com",
      "blogs": [
        { "title": "blog with relationship", "description": "description" },
        { "title": "blog with relationship", "description": "description" }
      ]
    }
  }
]

However, when I deploy this app to AWS Lambda using the following SAM template:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: An AWS Serverless Application Model template describing your function.
Resources:
  Full:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .
      Description: ''
      MemorySize: 128
      Timeout: 60
      Handler: src.main.handler
      Runtime: python3.12
      Architectures:
        - x86_64
      EphemeralStorage:
        Size: 512
      EventInvokeConfig:
        MaximumEventAgeInSeconds: 21600
        MaximumRetryAttempts: 2
      FunctionUrlConfig:
        AuthType: NONE
        InvokeMode: BUFFERED
      PackageType: Zip
      Policies:
        - Statement:
            - Effect: Allow
              Action:
                - logs:CreateLogGroup
              Resource: arn:aws:logs:us-east-1:820363156269:*
            - Effect: Allow
              Action:
                - logs:CreateLogStream
                - logs:PutLogEvents
              Resource:
                - >-
                  arn:aws:logs:us-east-1:820363156269:log-group:/aws/lambda/Full:*
      RecursiveLoop: Terminate
      SnapStart:
        ApplyOn: None
      RuntimeManagementConfig:
        UpdateRuntimeOn: Auto

I get a Too many redirects error when making a GET request to the generated URL:

https://cjosbvzkkcjiaa4e7frfwtacia0molag.lambda-url.us-east-1.on.aws/blogs

This also happens when testing in Postman, where it exceeds the max redirects limit.

However, making requests to specific blog IDs, like:

https://cjosbvzkkcjiaa4e7frfwtacia0molag.lambda-url.us-east-1.on.aws/blogs/1

works perfectly, returning the expected response.

If I test the app using the Test panel in the AWS Lambda console manager, using the AWS Proxy template:

{
  "body": "eyJ0ZXN0IjoiYm9keSJ9",
  "resource": "/{proxy+}",
  "path": "/blogs",
  "httpMethod": "GET",
  "isBase64Encoded": true,
  "queryStringParameters": {
    "foo": "bar"
  },
  "multiValueQueryStringParameters": {
    "foo": [
      "bar"
    ]
  },
  "pathParameters": {
    "proxy": "/blogs"
  },
  "stageVariables": {
    "baz": "qux"
  },
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate, sdch",
    "Accept-Language": "en-US,en;q=0.8",
    "Cache-Control": "max-age=0",
    "CloudFront-Forwarded-Proto": "https",
    "CloudFront-Is-Desktop-Viewer": "true",
    "CloudFront-Is-Mobile-Viewer": "false",
    "CloudFront-Is-SmartTV-Viewer": "false",
    "CloudFront-Is-Tablet-Viewer": "false",
    "CloudFront-Viewer-Country": "US",
    "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
    "Upgrade-Insecure-Requests": "1",
    "User-Agent": "Custom User Agent String",
    "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
    "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
    "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
    "X-Forwarded-Port": "443",
    "X-Forwarded-Proto": "https"
  },
  "multiValueHeaders": {
    "Accept": [
      "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
    ],
    "Accept-Encoding": [
      "gzip, deflate, sdch"
    ],
    "Accept-Language": [
      "en-US,en;q=0.8"
    ],
    "Cache-Control": [
      "max-age=0"
    ],
    "CloudFront-Forwarded-Proto": [
      "https"
    ],
    "CloudFront-Is-Desktop-Viewer": [
      "true"
    ],
    "CloudFront-Is-Mobile-Viewer": [
      "false"
    ],
    "CloudFront-Is-SmartTV-Viewer": [
      "false"
    ],
    "CloudFront-Is-Tablet-Viewer": [
      "false"
    ],
    "CloudFront-Viewer-Country": [
      "US"
    ],
    "Host": [
      "0123456789.execute-api.us-east-1.amazonaws.com"
    ],
    "Upgrade-Insecure-Requests": [
      "1"
    ],
    "User-Agent": [
      "Custom User Agent String"
    ],
    "Via": [
      "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"
    ],
    "X-Amz-Cf-Id": [
      "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="
    ],
    "X-Forwarded-For": [
      "127.0.0.1, 127.0.0.2"
    ],
    "X-Forwarded-Port": [
      "443"
    ],
    "X-Forwarded-Proto": [
      "https"
    ]
  },
  "requestContext": {
    "accountId": "123456789012",
    "resourceId": "123456",
    "stage": "prod",
    "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
    "requestTime": "09/Apr/2015:12:34:56 +0000",
    "requestTimeEpoch": 1428582896000,
    "identity": {
      "cognitoIdentityPoolId": null,
      "accountId": null,
      "cognitoIdentityId": null,
      "caller": null,
      "accessKey": null,
      "sourceIp": "127.0.0.1",
      "cognitoAuthenticationType": null,
      "cognitoAuthenticationProvider": null,
      "userArn": null,
      "userAgent": "Custom User Agent String",
      "user": null
    },
    "path": "/blogs",
    "resourcePath": "/{proxy+}",
    "httpMethod": "GET",
    "apiId": "1234567890",
    "protocol": "HTTP/1.1"
  }
}

I get this response:

{
  "statusCode": 307,
  "headers": {
    "content-length": "0",
    "location": "https://0123456789.execute-api.us-east-1.amazonaws.com/blogs/?foo=bar"
  },
  "multiValueHeaders": {},
  "body": "",
  "isBase64Encoded": false
}

Project Structure (simplified)

├── aws_lambda_artifact.zip
    └── src
        ├── main.py
        ├── routers
        │   └── blog.py
        ├── models.py
        └── database.py

Main.py

from fastapi import FastAPI
from mangum import Mangum

from src import models
from src.database import engine
from src.routers import blog, user

app = FastAPI()
handler = Mangum(app)

models.Base.metadata.create_all(bind=engine)

app.include_router(blog.router)

routers/blog.py

from typing import List

from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session

from src.database import get_db
from src.oauth2 import get_current_user
from src.repository import blog_repository
from src.schemas import ShowBlog, Blog, User

router = APIRouter(
    prefix="/blogs",
    tags=["blogs"]
)


@router.get("/", response_model=List[ShowBlog])
async def blogs(db: Session = Depends(get_db)):
    return blog_repository.get_all(db)


@router.get("/{blog_id}", response_model=ShowBlog)
async def get_blog(blog_id: int, db: Session = Depends(get_db)):
    return blog_repository.get(blog_id, db)


@router.post("/create-blog", status_code=status.HTTP_201_CREATED)
async def create_blog(request: Blog, db: Session = Depends(get_db)):
    return blog_repository.create(request, db)


@router.put("/{blog_id}", status_code=status.HTTP_202_ACCEPTED)
async def update_blog(blog_id: int, request: Blog, db: Session = Depends(get_db)):
    return blog_repository.update(blog_id, request, db)


@router.delete("/{blog_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_blog(blog_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
    return blog_repository.delete(blog_id, db)

Deployment Steps

  1. Installed dependencies to a dependencies/ folder:
    pip3 install -r requirements.txt --target=dependencies --python-version 3.12
    
  2. Added the dependencies and src directories to the .zip archive:
    (cd dependencies; zip ../aws_lambda_artifact.zip -r .)
    zip aws_lambda_artifact.zip -u -r src
    

Issue

The root /blogs endpoint is stuck in a redirect loop, but specific ID-based URLs (like /blogs/1) work fine. Any ideas why this is happening, and how I can resolve it?


Solution

  • As @MatsLindh noticed in their comment, the issue is likely due to the interaction between FastAPI's handling of trailing slashes and AWS Lambda's routing.

    FastAPI automatically performs a redirect (with a 307 status) when you visit an endpoint without a trailing slash, if the route has been defined with one. For example, if your route is defined as /blogs/ (with a trailing slash) but you request /blogs (without the trailing slash), FastAPI will redirect to /blogs/, which can cause issues in environments like AWS Lambda.

    Probable Cause:

    When deployed on AWS Lambda with a function URL, AWS adds an additional layer of routing, and the function URL endpoint might not handle trailing slashes in the same way as when running locally via Uvicorn. This can result in an infinite redirect loop, because:

    1. FastAPI expects /blogs/ but gets /blogs.
    2. AWS Lambda handles the redirect incorrectly, sending the same request back to /blogs, triggering FastAPI's redirect behavior again.

    Solution:

    1. Remove Trailing Slashes in Routes: Modify the route definitions to exclude trailing slashes. In your case, change the route definition for the blogs endpoint like this:

      router = APIRouter(
          prefix="/blogs",  
          tags=["blogs"]
      )
      
      @router.get("")  # Remove the trailing slash in the route definition
      async def blogs(db: Session = Depends(get_db)):
          return blog_repository.get_all(db)
      

      This way, FastAPI will not attempt to redirect when the request is made without a trailing slash, avoiding the redirect loop.