Giter VIP home page Giter VIP logo

apig-wsgi's Introduction

apig-wsgi

https://img.shields.io/github/actions/workflow/status/adamchainz/apig-wsgi/main.yml.svg?branch=main&style=for-the-badge https://img.shields.io/badge/Coverage-100%25-success?style=for-the-badge https://img.shields.io/pypi/v/apig-wsgi.svg?style=for-the-badge https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge pre-commit

Wrap a WSGI application in an AWS Lambda handler function for running on API Gateway or an ALB.

A quick example:

from apig_wsgi import make_lambda_handler
from myapp.wsgi import app

# Configure this as your entry point in AWS Lambda
lambda_handler = make_lambda_handler(app)

Improve your Django and Git skills with my books.


Installation

Use pip:

python -m pip install apig-wsgi

Python 3.8 to 3.12 supported.

Usage

Use apig-wsgi in your AWS Lambda Function that you attach to one of:

Both “format version 1” and “format version 2” are supported (documentation). apig-wsgi will automatically detect the version in use. At time of writing, “format version 2” is used for Lambda Function URL’s and API Gateway HTTP API’s.

make_lambda_handler(app, binary_support=None, non_binary_content_type_prefixes=None)

app should be a WSGI app, for example from Django's wsgi.py or Flask's Flask() object.

binary_support configures whether responses containing binary are supported. The default, None, means to automatically detect this from the format version of the event - on it defaults to True for format version 2, and False for format version 1. Depending on how you're deploying your lambda function, you may need extra configuration before you can enable binary responses:

  • ALB’s support binary responses by default.
  • API Gateway HTTP API’s support binary responses by default (and default to event format version 2).
  • API Gateway REST API’s (the “old” style) require you to add '*/*' in the “binary media types” configuration. You will need to configure this through API Gateway directly, CloudFormation, SAM, or whatever tool your project is using. Whilst this supports a list of binary media types, using '*/*' is the best way to configure it, since it is used to match the request 'Accept' header as well, which WSGI applications often ignore. You may need to delete and recreate your stages for this value to be copied over.

Note that binary responses aren't sent if your response has a 'Content-Type' starting 'text/', 'application/json' or 'application/vnd.api+json' - this is to support sending larger text responses, since the base64 encoding would otherwise inflate the content length. To avoid base64 encoding other content types, you can set non_binary_content_type_prefixes to a list or tuple of content type prefixes of your choice (which replaces the default list).

If the event from API Gateway contains the requestContext key, for example on format version 2 or from custom request authorizers, this will be available in the WSGI environ at the key apig_wsgi.request_context.

If you want to inspect the full event from API Gateway, it's available in the WSGI environ at the key apig_wsgi.full_event.

If you need the Lambda Context object, it's available in the WSGI environ at the key apig_wsgi.context.

If you’re using “format version 1”, multiple values for request and response headers and query parameters are supported. They are enabled automatically on API Gateway but need explict activation on ALB’s. If you need to determine from within your application if multiple header values are enabled, you can can check the apgi_wsgi.multi_value_headers key in the WSGI environ, which is True if they are enabled and False otherwise.

Example

An example Django project with Ansible deployment is provided in the example/ directory in the repository. See the README.rst there for guidance.

apig-wsgi's People

Contributors

adamchainz avatar asottile avatar cox65 avatar dependabot[bot] avatar irgeek avatar lindycoder avatar pre-commit-ci[bot] avatar reissm avatar seporaitis avatar tobiasmcnulty avatar v1k45 avatar waderobson avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

apig-wsgi's Issues

Unhandled exceptions

Hello,
We had a problem found during development where the V1Response method threw an exception. It was an easy fix, but the stack trace wasn't particularly useful.

So I was wondering if you had any thoughts or opinion on adding a catch-all exception handler that logged useful information from the event data?

It is something I could submit a PR for, just wanted to ask about before I do it.

Thanks,
Jason

ELB Health Checks not handled

Hello!

Thank you for your tool it's great.

We have a little issue when using a lambda as an ELB target when we turn on health check because the event looks like this:

{
    "requestContext": {
        "elb": {
            "targetGroupArn": "arn:aws:elasticloadbalancing:region:123456789012:targetgroup/my-target-group/6d0ecf831eec9f09"
        }
    },
    "httpMethod": "GET",  
    "path": "/",  
    "queryStringParameters": {},  
    "headers": {
        "user-agent": "ELB-HealthChecker/2.0"
    },  
    "body": "",  
    "isBase64Encoded": false
}

source: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#enable-health-checks-lambda

This has almost no headers leading to this error

  File "/var/task/flask/app.py", line 2306, in wsgi_app
    ctx = self.request_context(environ)
  File "/var/task/flask/app.py", line 2223, in request_context
    return RequestContext(self, environ)
  File "/var/task/flask/ctx.py", line 284, in __init__
    self.url_adapter = app.create_url_adapter(self.request)
  File "/var/task/flask/app.py", line 2040, in create_url_adapter
    subdomain=subdomain)
  File "/var/task/werkzeug/routing.py", line 1502, in bind_to_environ
    wsgi_server_name = get_host(environ).lower()
  File "/var/task/werkzeug/wsgi.py", line 168, in get_host
    rv = environ["SERVER_NAME"]

i found with some testing that i can patch the event like this to make it work:

def elb_health_check_patcher(handler):
    @wraps(handler)
    def wrapper(event, context):
        user_agent = event.get('headers', {}).get('user-agent')
        if user_agent == "ELB-HealthChecker/2.0":
            event['headers']['x-forwarded-proto'] = 'http'
            event['headers']['host'] = 'elb-health-check'

        return handler(event, context)

    return wrapper

handler = elb_health_check_patcher(make_lambda_handler(my_app))

I may propose a PR soon if you're open to :)

Get error "Unable to import module 'wsgi': cannot import name 'app_ctx' from 'flask.globals' "

Python Version

3.8

Package Version

2.17

Description

I get this error when running my web site as an AWS lambda service. I do not get ithis error when using the same code on EC2 using gunicorn and wsgi using the same code base and requirements file

[ERROR] Runtime.ImportModuleError: Unable to import module 'wsgi': cannot import name 'app_ctx' from 'flask.globals' (/var/task/flask/globals.py)

flask modules:

Flask==2.0.3
Flask-WTF==1.0.1
Flask-Login==0.6.1
Flask-Migrate==2.0.2
Flask-Script==2.0.5
Flask-SQLAlchemy==2.5.1
Flask-BabelEx==0.9.4
Flask-User==1.0.2.2
Flask-Session==0.4.0
Flask-Cors==3.0.10

API Gateway call results in [ERROR] KeyError: 'path'

Python Version

3.8

Package Version

2.12.1

Description

I'm trying to call this really simple app with API Gateway (REST) Lambda integration:

#!/usr/bin/python3
import urllib3
from flask import Flask
from apig_wsgi import make_lambda_handler

app = Flask(__name__)
lambda_handler = make_lambda_handler(app)

@app.route('/eventprint', methods=['GET'])
def print_event():
    return {"status": 200, "message": "OK"}

if __name__ == "__main__":
    app.run(debug=True)

but I got the following error:

[ERROR] KeyError: 'path'
Traceback (most recent call last):
  File "/var/lang/lib/python3.8/site-packages/apig_wsgi.py", line 45, in handler
    environ = get_environ_v1(
  File "/var/lang/lib/python3.8/site-packages/apig_wsgi.py", line 73, in get_environ_v1
    "PATH_INFO": urllib.parse.unquote(event["path"], encoding="iso-8859-1"),

The application works with ALB.
Has anybody run into this issue before? What am I doing wrong?

make_event - questions and feature request

Python Version

No response

Package Version

No response

Description

Firstly, thank you for sharing apig-wsgi on github. At the point I realised I needed such a module I found this and it has been great.

It enables me to deploy Flask apps as lambda's without having to use Zappa so now have much more control over deployment of lambdas to AWS with only two lines of additional code. We can also deploy as container images. Our api's are first designed in openapi and then code generated to create a flask app, so nearly all our lambdas are wsgi apps. I find this makes them easier to manage.

My question is around the make_v1_event from the test module which I have also found really useful. I used it to create an event for the invocation of a lambda on AWS that is private (in a VPC) and has a private api gateway which cannot be called from outside the VPC. 

This has made me question if when calling private API's from inside the VPC whether there is any need to use the private API gateway at all. It would be more efficient to invoke the lambda event directly. This is where the make_v1_event could come in even more useful.

I have a couple of questions and a feature request:

Have you ever had any requirement to use this function outside of the test module?

Do you think there are any downsides to bypassing the gateway (other than losing any of the additional features only offered by using apig)?

Would you consider making it possible to import this function from apig-wsgi? i.e.

from apig_wsgi import make_v1_event

event = make_v1_event(path="/",  method="GET")

response = awslambda.invoke(  
  FunctionName='lambda_function',  
  InvocationType='RequestReposnse',  
  Payload=json.dumps(event)
)

It would certainly be useful for me, maybe others too?

QueryStringParameters encoding issue

Hi,

I suspect an issue while providing queryParameters.

When performing a query to my API using <API_ROOT>/logs?start_time=2020-10-12T14:00:00%2B02:00&end_time=2020-10-12T15:00:00%2B02:00

I get my query string values in Flask endpoint as:
start_time = "2020-10-12T14:00:00 02:00"
end_time = "2020-10-12T15:00:00 02:00"

and it should be :
start_time = "2020-10-12T14:00:00+02:00"
end_time = "2020-10-12T15:00:00+02:00"

Note: The "+" has been removed

I think this is because API Gateway is providing queryStringParameters decoded and your are not encoding them back to store them in environ["QUERY_STRING"].
If we look to what other WSGI adapters (like Zappa) are doing:

       if 'multiValueQueryStringParameters' in event_info:
            query = event_info['multiValueQueryStringParameters']
            query_string = urlencode(query, doseq=True) if query else ''
        else:
            query = event_info.get('queryStringParameters', {})
            query_string = urlencode(query) if query else ''

Hope it helps.

Support exc_info

Do something sane when the WSGI app calls start_response with exc_info

Error when requestContext is None

Python Version

3.8

Package Version

2.12.0

Description

I have a lambda that is called through API Gateway, but can also be invoked directly via a lambda client. Upgrading to 2.12.0 started causing this error when invoked via client.

[ERROR] TypeError: argument of type 'NoneType' is not iterable
Traceback (most recent call last):
  File "/var/task/apig_wsgi/__init__.py", line 61, in handler
    if "requestContext" in event and "elb" in event["requestContext"]:

This is being done from Kotlin code, using com.amazonaws.services.lambda.AWSLambda client. When an AwsProxyRequest object is constructed, it has requestContext=null by default. This results in the error above.

I can update my invoke code to set a dummy requestContext, but I do believe it is valid to be None in the direct lambda invocation use case. Does it make sense to add a not None check here for requestContext?

Thanks.

Ability to have custom white list for non binary content types

Problem

The Response class has a whitelist of "non binary content types" which includes:

  • text/*
  • application/json

non_binary_content_types = ("text/", "application/json")

All other response types are assumed to be binary if binary_support is True.

This becomes a problem if you want to return text content for other content types. For example, JSON:API APIs have application/vnd.api+json content type.

Possible solution

The library should provide ability to either extend the Response class or pass the non_binary_content_types directly to make_lambda_handler call.

wsgi_handler = make_lambda_handler(application, binary_support=True, response_class=MyResponse)

wsgi_handler = make_lambda_handler(application, binary_support=True, non_binary_content_types=my_content_type_list)

I can create PR for either of these approaches.

What do you think?

Support format version 2 for HTTP API's

https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html

Extended the example to deploy an HTTP API and dump its event, got one like this, plus error from trying to parse format version 1:

START RequestId: 863fef15-563d-498f-8d32-a4a5737f014f Version: $LATEST                                                                                                                                                                                                                                                                                                                                
{                                                                                                                                                                                                                                                                                                                                                                                                     
  "cookies": [                                                                                                                                                                                                                                                                                                                                                                                        
    "pN=7",                                                                                                                                                                                                                                                                                                                                                                                           
  ],                                                                                                                                                                                                                                                                                                                                                                                                  
  "headers": {                                                                                                                                                                                                                                                                                                                                                                                        
    "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",                                                                                                                                                                                                                                                                                                           
    "accept-encoding": "gzip, deflate, br",                                                                                                                                                                                                                                                                                                                                                           
    "accept-language": "en-GB,en;q=0.5",                                                                                                                                                                                                                                                                                                                                                              
    "content-length": "0",                                                                                                                                                                                                                                                                                                                                                                            
    "dnt": "1",                                                                                                                                                                                                                                                                                                                                                                                       
    "host": "s219lqqg4f.execute-api.eu-central-1.amazonaws.com",                                                                                                                                                                                                                                                                                                                                      
    "upgrade-insecure-requests": "1",                                                                                                                                                                                                                                                                                                                                                                 
    "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:73.0) Gecko/20100101 Firefox/73.0",                                                                                                                                                                                                                                                                                               
    "x-amzn-trace-id": "Root=1-5e6b67ad-2d4a5560cf6fb7a02780c3a0",                                                                                                                                                                                                                                                                                                                                    
    "x-forwarded-for": "91.109.238.159",                                                                                                                                                                                                                                                                                                                                                              
    "x-forwarded-port": "443",                                                                                                                                                                                                                                                                                                                                                                        
    "x-forwarded-proto": "https"                                                                                                                                                                                                                                                                                                                                                                      
  },                                                                                                                                                                                                                                                                                                                                                                                                  
  "isBase64Encoded": true,                                                                                                                                                                                                                                                                                                                                                                            
  "rawPath": "/",                                                                                                                                                                                                                                                                                                                                                                                     
  "rawQueryString": "",                                                                                                                                                                                                                                                                                                                                                                               
  "requestContext": {                                                                                                                                                                                                                                                                                                                                                                                 
    "accountId": "931198139388",                                                                                                                                                                                                                                                                                                                                                                      
    "apiId": "s219lqqg4f",                                                                                                                                                                                                                                                                                                                                                                            
    "domainName": "s219lqqg4f.execute-api.eu-central-1.amazonaws.com",                                                                                                                                                                                                                                                                                                                                
    "domainPrefix": "s219lqqg4f",                                                                                                                                                                                                                                                                                                                                                                     
    "http": {                                                                                                                                                                                                                                                                                                                                                                                         
      "method": "GET",                                                                                                                                                                                                                                                                                                                                                                                
      "path": "/",                                                                                                                                                                                                                                                                                                                                                                                    
      "protocol": "HTTP/1.1",                                                                                                                                                                                                                                                                                                                                                                         
      "sourceIp": "91.109.238.159",                                                                                                                                                                                                                                                                                                                                                                   
      "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:73.0) Gecko/20100101 Firefox/73.0"                                                                                                                                                                                                                                                                                               
    },                                                                                                                                                                                                                                                                                                                                                                                                
    "requestId": "JU0jFiJxliAEJHg=",                                                                                                                                                                                                                                                                                                                                                                  
    "routeId": null,                                                                                                                                                                                                                                                                                                                                                                                  
    "routeKey": "$default",                                                                                                                                                                                                                                                                                                                                                                           
    "stage": "$default",                                                                                                                                                                                                                                                                                                                                                                              
    "time": "13/Mar/2020:10:59:57 +0000",                                                                                                                                                                                                                                                                                                                                                             
    "timeEpoch": 1584097197293                                                                                                                                                                                                                                                                                                                                                                        
  },                                                                                                                                                                                                                                                                                                                                                                                                  
  "routeKey": "$default",                                                                                                                                                                                                                                                                                                                                                                             
  "version": "2.0"                                                                                                                                                                                                                                                                                                                                                                                    
}                                                                                                                                                                                                                                                                                                                                                                                                     
[ERROR] KeyError: 'httpMethod' Traceback (most recent call last):   File "/var/task/testapp/wsgi.py", line 16, in lambda_handler     return apig_wsgi_handler(event, context)   File "/var/task/apig_wsgi.py", line 37, in handler     environ = get_environ(event, context, binary_support=binary_support)   File "/var/task/apig_wsgi.py", line 50, in get_environ     method = event["httpMethod"] 

Fails with `/` route

Python Version

3.9.6

Package Version

2.12.1

Description

Summary

When the API Gateway has a path of simply /, the library returns:

[ERROR] KeyError: 'path'
Traceback (most recent call last):
  File "/var/lang/lib/python3.9/site-packages/apig_wsgi/__init__.py", line 73, in handler
    environ = get_environ_v1(
  File "/var/lang/lib/python3.9/site-packages/apig_wsgi/__init__.py", line 107, in get_environ_v1
    "PATH_INFO": urllib.parse.unquote(event["path"], encoding="iso-8859-1"),

This is related to, but not the same as #292, which was trivially solved using a wildcard route ie /{proxy+}. However this bug concerns the case where you specifically need the root route to work.

Replication

Unfortunately I don't have any CloudFormation code just for the moment, but I can describe my resources:

$ aws apigateway get-resources --rest-api-id <my-api-id>
{
    "items": [
        {
            "id": "ccgexbzsj1",
            "path": "/",
            "resourceMethods": {
                "ANY": {}
            }
        }
    ]
}

Interestingly, even when I remove the root resource, and add a wildcard route, this bug still happens if I hit /: ignore this, the proxy was incorrectly configured here. With this config, hitting / simply fails to find a handler and you get {"message":"Missing Authentication Token"}:

{
    "items": [
        {
            "id": "893apb",
            "parentId": "ccgexbzsj1",
            "pathPart": "{proxy+}",
            "path": "/{proxy+}",
            "resourceMethods": {
                "ANY": {}
            }
        },
        {
            "id": "ccgexbzsj1",
            "path": "/"
        }
    ]
}

502 bad gateway error on ALB : statusCode must be numeric

Hey,

having an issue with apig-wsgi on an ALB : I always receive a 502 bad gateway error.
Changing statusCode from a string to a number solve my issue.

This is my working monkey patching :

import apig_wsgi
class Response(apig_wsgi.Response):
def as_apig_response(self):
response = super().as_apig_response()
response['statusCode'] = int(response['statusCode'])
return response
apig_wsgi.Response = Response

Test 2

Python Version

No response

Package Version

No response

Description

yada

Test

Python Version

No response

Package Version

No response

Description

test

Issue with multi stage APIG

Hi !

Working with multi-stage and custom domain name in api gateway is hard on the path side of thing !

1/ First, i discovered that default mapping works only when there is one mapping .... otherwise all mapping requires an explicit key mapping set, even the default. Ok, fair enough : routing has to be done consistently.

But the next i discover, is on an another level.

2/ api gateway insists on mangling the path, prefixing it with the stage name.
/status become /{stage}/status

Not really good. Shall i really remap all my application routes to handle stages now ? Hooray for separation of concern ...
Still, could be logical, after all this is what the incoming request was made up.

3/ Right ? No. This mangling doesn't happen on $default stage. Incredible. We've just setup a key mapping for default because we had to, see /1, but now the key mapping is dropped of the path for default !

Now i need to handle 2 routes for each resource : one with prefix, one without. Clever ? Not at all.

In fact the prefix given as a key mapping has no impact on the path : it's always overwritten by the stage name or nothing for the default stage. So easy to debug, if you've not planned to use the stage name as the key mapping ...

4/ I'm already crying down the floor. But i won't stop trying. Forget key mapping, let's deploy each stage as default of it's own domain ! It might work ! Maybe ? No. Same sad result.

Really, don't leave me 30s in the same room as the guy who developed this API .... i will not refrain from anything.

Enough ranting, let's fix this. So let's add another middleware.
Or as we have standardized on apig-wsgi, could we add a de-bullshit-stage-path option to it ?

Not much needed on the dev side : just drop /{stage} from the beginning of the path if stage is not $default and drop_stage_prefix parameter is set.

@adamchainz, would you take a patch for this ?

CU
Antoine

stage portion of URL not passed in "path"

I am using a Django REST Framework JSON-API app that embeds URLs in the response and the problem is that the stage portion of the API GW URL is not passed in to the app so the URLs returned are missing the stage. For example:

GET https://redacted.execute-api.us-east-1.amazonaws.com/Prod/v1/standards returns a paginated JSON response body like this:

{
    "links": {
        "first": "https://redacted.execute-api.us-east-1.amazonaws.com/v1/standards?page%5Bnumber%5D=1",
        "last": "https://redacted.execute-api.us-east-1.amazonaws.com/v1/standards?page%5Bnumber%5D=63",
        "next": "https://redacted.execute-api.us-east-1.amazonaws.com/v1/standards?page%5Bnumber%5D=2",
        "prev": null
    },
    "data": [
        {
            "type": "Standard",
            "id": "891edfca-fb09-42d6-aee9-b182de008978",

...

The URLs are missing "/Prod" because the event["path"] does not include the stage.

I've looked at kludging up some changes in apig_wsgi.py such as concatenating the event["requestContext"]["stage"] and adding a prefix environment variable to the django app's urlpatterns, but wanted to check in here first to see if I'm just not configuring something correctly.

Here's my lambda entry point code:

import os
from apig_wsgi import make_lambda_handler

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tech_stds_catalog.settings')

app = get_wsgi_application()
lambda_handler = make_lambda_handler(app)

test

Python Version

No response

Package Version

No response

Description

test

Cookies not being set with AWS SAM HttpApi running locally

Python Version

3.8

Package Version

2.11.0

Description

Running locally with SAM using sam local start-api and a V2 HttpApi event source. cookies aren't being set. For context, I'm running a Flask app in a Lambda container image. Cookie headers are sent correctly when using the V1 Api event source.

Test

Python Version

3.9.6

Package Version

2.11.0

Description

Testing issue form.

Provisioned Lambda still as slow as a cold start

Thanks for the great project!

Probably not an issue per se with apig-wsgi, but perhaps if you could give some guidance.

I have a massive django app, which I'm trying to run on the new Lambda containers feature.

It takes a while to start, the docker image is big and with so many models etc the app takes a few seconds to initialise

I thought provisioning would "warm" the app up so there would be no delay, however the delay is still there.

Any tips for this?

My handler is pretty simple:

from apig_wsgi import make_lambda_handler
from app.wsgi import application

# Configure this as your entry point in AWS Lambda
lambda_handler = make_lambda_handler(application)

Cookies need to be unescaped

I ran into an issue with cookies and for HttpAPI requests both V1 and V2 formats.

Cookies that have escaped values (like doubles quotes) come escaped with backslashes, and they need to be unescaped for applications to be able to read them properly.

An example of a cookie that will present problems is:

cookie: escaped cookie=this cookie has an escaped double quote \"; cookie with commas=This cookie has an escaped comma \054

The cookies array in the V2 event will be:

...
    "cookies": [
        'escaped cookie=this cookie has an escaped double quote \\"',
        'cookie with comma=This cookie has an escaped comma \\054',
    ],
...

The values need to be unescaped, otherwise WSGI applications will read the two backslashes.

Solutions to the problem are discussed here:
https://stackoverflow.com/questions/1885181/how-to-un-escape-a-backslash-escaped-string/57192592#57192592

The following works for V2 requests:

from codecs import encode, decode
...
        "HTTP_COOKIE": decode(encode(";".join(event.get("cookies", ())), "latin-1", "backslashreplace"), "unicode-escape")
...

Query parameters not url decoded?

I have an application using Bottle that I've tested standalone and in AWS with an ALB. When run standalone the query parameters are url decoded, when running in AWS/ALB they don't seem to be. As an example, I get a query parameter of a UK phone number. When using Bottle's default http server I get the parameter as "+4412345678", when running in AWS I get it as "%2B4412345678". Currently my code attempts to detect and strip off the "+44" before using it to match a record in a database. Should I be manually decoding the query parameters when using apig-wsgi or is this a bug?

Problem with binary files

I'm trying to return an Excel file as an attachment. Here's my code:

content_type = (
    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
response = HttpResponse(content=data, content_type=content_type)
response["Content-Disposition"] = 'attachment; filename="{0}"'.format(filename)
return response

First I had an encoding error:

'utf-8' codec can't decode byte 0xc0 in position 10: invalid start byte: UnicodeDecodeError
Traceback (most recent call last):
  File "/opt/apig_wsgi.py", line 44, in handler
    return response.as_apig_response()
  File "/opt/apig_wsgi.py", line 149, in as_apig_response
    response["body"] = self.body.getvalue().decode("utf-8")
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc0 in position 10: invalid start byte

So I activated the binary mode:

app = make_lambda_handler(application, binary_support=True)

I have no more errors but my file can't be read :(

Query Params - difference between API GW and ALB

I noticed if I go to a URL with a query string like this: ?x=foo%3Dbar

The event sent to a lambda from ALB contains:

"queryStringParameters":  {
    "x": "foo%3Dbar"
}

And the event sent from API Gateway contains:

"queryStringParameters":  {
    "x": "foo=bar"
}

For the ALB, the QUERY_STRING in the WSGI environment is set to:

x=foo%253Dbar

And for API Gateway, it is set to:

x=foo%3Dbar

So I think the processing of query params for ALB needs to not urlencode the data.

I am working on a PR for this and will hopefully submit later today.

2.5.0 Multivalue support breaks Host Header support

The new update for 2.5.0 broke for us, because we're trying to access the HTTP Host header directly, rather than accessing via the SERVER_NAME WSGI variable.

Was this intentional, or just a byproduct of trying to also set SERVER_NAME for WSGI spec compliance?

Binary responses not enabled for ALB by default

Python Version

3.10

Package Version

2.15.0

Description

Following the documentation, ALB always supports binary response types, but the wrapper does not enable binary responses by default.

This leads to 502 responses due to a raised error from L322.

Would it be an option to enable binary responses for ALB by default?

Ability to send binary response with `text/` or `application/json` content type

Problem

Response.as_apig_response is hardcoded to not return binary response when the content-type of the response is either text/* or application/json.

apig-wsgi/apig_wsgi.py

Lines 105 to 109 in b0ce56c

content_type = self._get_content_type()
should_send_binary = self.binary_support and (
content_type is None
or not content_type.startswith(("text/", "application/json"))
)

This becomes a problem for use cases like returning a gzip response from the application with the above content types.

Possible Solution

Check if the Content-Encoding header has gzip in it or not, and return the binary response based on that.

This fix would keep the hardcoded values, something like this should work:

    def _should_send_binary(self):
        """Determines if binary response should be sent to API Gateway
        """
        non_binary_content_types = ("text/", "application/json")

        content_type = self._get_content_type() or ''
        content_encoding = self._get_content_encoding() or ''

        supports_binary = self.binary_support
        is_binary_content_type = not content_type.startswith(non_binary_content_types)

        if supports_binary and is_binary_content_type:
            return True
        # Content type is non-binary but the content encoding is.
        elif supports_binary and not is_binary_content_type:
            return 'gzip' in content_encoding.lower()

        return False

Allow Response class to be extended so that users can handle this logic on their own.

This can be done by introducing the same Response._should_send_binary method which defaults to the current behaviour. The make_lambda_handler function can then be allowed to have an argument like response_class to facilitate this.

I can create a PR for either of the fixes which seem more appropriate to you.

What do you think about this?

Handling trailing slashes that get omitted in event property

Python Version

3.9

Package Version

Latest

Description

Flagging this as somewhere between issue discussion and possible / proposed fix — I noticed in my project that, when requesting a particular URL with a trailing slash, event['requestContext']['http']['path'] coming from AWS will omit the trailing slash. I lost a couple hours digging into redirect loop ( in Django, repeatedly trying to redirect to the admin login screen with its trailing slash in the URL, but never getting there because AWS's event doesn't pass that final slash along ) before finally discovering the issue.

Interestingly, another event property, event['rawPath'] will correctly pass along that trailing slash. Don't know if it is documented anywhere in AWS world why one ( the "raw" path ) would include it, and the other would omit it, but after confirming this discrepancy side by side, I switched to another library which I found does use rawPath and it worked without issue. 💁🏻‍♂️

This feels like AWS's bug to fix ultimately ( either in code for consistency's sake, or at least documentation; ideally both ), and no interest or need in pitting two open source projects against eachother, but using rawPath or some derivative could be a solution here.

Worth noting for my particular instance, Django has a whole APPEND_SLASH setting and middleware to handle this, but the admin redirect logic I mentioned kicks in before this fallback does. Maybe my scenario is too narrow / niche to warrant a code change 💁🏻‍♂️ but the admin's a pretty prominent cornerstone of using Django to begin with, and, anyway anyway ... mostly wanted to get this issue out of just my own brain, and hope this might help save someone a couple hours in the future. 👋🏻

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.