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
}
├── aws_lambda_artifact.zip
└── src
├── main.py
├── routers
│ └── blog.py
├── models.py
└── database.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)
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)
dependencies/
folder:
pip3 install -r requirements.txt --target=dependencies --python-version 3.12
dependencies
and src
directories to the .zip
archive:
(cd dependencies; zip ../aws_lambda_artifact.zip -r .)
zip aws_lambda_artifact.zip -u -r src
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?
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.
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:
/blogs/
but gets /blogs
./blogs
, triggering FastAPI's redirect behavior again.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.