├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── chi ├── adapter.go ├── adapterv2.go ├── chi_suite_test.go └── chilambda_test.go ├── core ├── core_suite_test.go ├── request.go ├── requestALB.go ├── requestALB_test.go ├── request_test.go ├── requestv2.go ├── requestv2_test.go ├── response.go ├── responseALB.go ├── responseALB_test.go ├── response_test.go ├── responsev2.go ├── responsev2_test.go ├── switchablerequest.go ├── switchablerequest_test.go ├── switchableresponse.go ├── switchableresponse_test.go ├── types.go ├── typesALB.go └── typesv2.go ├── echo ├── adapter.go ├── adapterALB.go ├── adapterv2.go ├── echo_suite_test.go └── echolambda_test.go ├── examples └── fiber │ └── main.go ├── fiber ├── adapter.go ├── fiber_suite_test.go └── fiberlambda_test.go ├── gin ├── adapter.go ├── adapterALB.go ├── adapterv2.go ├── gin_suite_test.go └── ginlambda_test.go ├── go.mod ├── go.sum ├── gorillamux ├── adapter.go ├── adapterALB.go ├── adapterALB_test.go ├── adapter_test.go ├── adapterv2.go ├── adapterv2_test.go └── gorilla_suite_test.go ├── handlerfunc ├── adapter.go ├── adapterALB.go ├── adapterALB_test.go ├── adapter_test.go ├── adapterv2.go ├── adapterv2_test.go └── handlerfunc_suite_test.go ├── httpadapter ├── adapter.go ├── adapterALB.go ├── adapterALB_test.go ├── adapter_test.go ├── adapterv2.go ├── adapterv2_test.go └── handlerfunc_suite_test.go ├── iris ├── adapter.go ├── iris_suite_test.go └── irislambda_test.go ├── negroni ├── adapter.go ├── adapter_test.go └── negroni_suite_test.go └── sample ├── go.mod ├── go.sum ├── main.go ├── pets.go └── sam.yaml /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .vscode 15 | .idea 16 | vendor/*/ 17 | gin/aws-lambda-go-api-proxy-gin 18 | core/aws-lambda-go-api-proxy-core 19 | sample/main 20 | sample/output-sam.yaml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.x 5 | env: 6 | - GO111MODULE=on -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/awslabs/aws-lambda-go-api-proxy/issues), or [recently closed](https://github.com/awslabs/aws-lambda-go-api-proxy/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/aws-lambda-go-api-proxy/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/awslabs/aws-lambda-go-api-proxy/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Go parameters 2 | GOCMD=go 3 | GOBUILD=$(GOCMD) build 4 | GOCLEAN=$(GOCMD) clean 5 | GOTEST=$(GOCMD) test 6 | GOGET=$(GOCMD) get 7 | CORE_BINARY_NAME=aws-lambda-go-api-proxy-core 8 | GIN_BINARY_NAME=aws-lambda-go-api-proxy-gin 9 | SAMPLE_BINARY_NAME=main 10 | 11 | all: clean test build package 12 | build: 13 | $(GOBUILD) ./... 14 | cd sample && $(GOBUILD) -o $(SAMPLE_BINARY_NAME) 15 | package: 16 | cd sample && zip main.zip $(SAMPLE_BINARY_NAME) 17 | test: 18 | $(GOTEST) -v ./... 19 | clean: 20 | rm -f sample/$(SAMPLE_BINARY_NAME) 21 | rm -f sample/$(SAMPLE_BINARY_NAME).zip -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | AWS Lambda Go Api Proxy 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AWS Lambda Go API Proxy [![Build Status](https://travis-ci.org/awslabs/aws-lambda-go-api-proxy.svg?branch=master)](https://travis-ci.org/awslabs/aws-lambda-go-api-proxy) 2 | aws-lambda-go-api-proxy makes it easy to run Go APIs written with frameworks such as [Gin](https://github.com/gin-gonic/gin) with AWS Lambda and Amazon API Gateway. 3 | 4 | ## Getting started 5 | 6 | Install required dependencies. 7 | 8 | ```bash 9 | # First, install the Lambda go libraries. 10 | $ go get github.com/aws/aws-lambda-go/events 11 | $ go get github.com/aws/aws-lambda-go/lambda 12 | 13 | # Next, install the core library. 14 | $ go get github.com/awslabs/aws-lambda-go-api-proxy/... 15 | ``` 16 | 17 | ### Standard library 18 | 19 | To use with the standard library, the `httpadaptor.New` function takes in a `http.Handler`. The `ProxyWithContent` method on the `httpadapter.HandlerAdapter` can then be used as a Lambda handler. 20 | 21 | ```go 22 | package main 23 | 24 | import ( 25 | "io" 26 | "net/http" 27 | 28 | "github.com/aws/aws-lambda-go/lambda" 29 | "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" 30 | ) 31 | 32 | func main() { 33 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 34 | io.WriteString(w, "Hello") 35 | }) 36 | 37 | lambda.Start(httpadapter.New(http.DefaultServeMux).ProxyWithContext) 38 | } 39 | ``` 40 | 41 | ### Gin 42 | 43 | To use with the Gin framework, following the instructions from the [Lambda documentation](https://docs.aws.amazon.com/lambda/latest/dg/go-programming-model-handler-types.html), declare a `Handler` method for the main package. 44 | 45 | Declare a `ginadapter.GinLambda` object in the global scope, and initialize it in the `init` function, adding all API methods. 46 | 47 | The `ProxyWithContext` method is then used to translate requests and responses. 48 | 49 | ```go 50 | package main 51 | 52 | import ( 53 | "log" 54 | "context" 55 | 56 | "github.com/aws/aws-lambda-go/events" 57 | "github.com/aws/aws-lambda-go/lambda" 58 | "github.com/awslabs/aws-lambda-go-api-proxy/gin" 59 | "github.com/gin-gonic/gin" 60 | ) 61 | 62 | var ginLambda *ginadapter.GinLambda 63 | 64 | func init() { 65 | // stdout and stderr are sent to AWS CloudWatch Logs 66 | log.Printf("Gin cold start") 67 | r := gin.Default() 68 | r.GET("/ping", func(c *gin.Context) { 69 | c.JSON(200, gin.H{ 70 | "message": "pong", 71 | }) 72 | }) 73 | 74 | ginLambda = ginadapter.New(r) 75 | } 76 | 77 | func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 78 | // If no name is provided in the HTTP request body, throw an error 79 | return ginLambda.ProxyWithContext(ctx, req) 80 | } 81 | 82 | func main() { 83 | lambda.Start(Handler) 84 | } 85 | ``` 86 | 87 | ### Fiber 88 | 89 | To use with the Fiber framework, following the instructions from the [Lambda documentation](https://docs.aws.amazon.com/lambda/latest/dg/go-programming-model-handler-types.html), declare a `Handler` method for the main package. 90 | 91 | Declare a `fiberadapter.FiberLambda` object in the global scope, and initialize it in the `init` function, adding all API methods. 92 | 93 | The `ProxyWithContext` method is then used to translate requests and responses. 94 | 95 | ```go 96 | // main.go 97 | package main 98 | 99 | import ( 100 | "context" 101 | "log" 102 | 103 | "github.com/aws/aws-lambda-go/events" 104 | "github.com/aws/aws-lambda-go/lambda" 105 | fiberadapter "github.com/awslabs/aws-lambda-go-api-proxy/fiber" 106 | "github.com/gofiber/fiber/v2" 107 | ) 108 | 109 | var fiberLambda *fiberadapter.FiberLambda 110 | 111 | // init the Fiber Server 112 | func init() { 113 | log.Printf("Fiber cold start") 114 | var app *fiber.App 115 | app = fiber.New() 116 | 117 | app.Get("/", func(c *fiber.Ctx) error { 118 | return c.SendString("Hello, World!") 119 | }) 120 | 121 | fiberLambda = fiberadapter.New(app) 122 | } 123 | 124 | // Handler will deal with Fiber working with Lambda 125 | func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 126 | // If no name is provided in the HTTP request body, throw an error 127 | return fiberLambda.ProxyWithContext(ctx, req) 128 | } 129 | 130 | func main() { 131 | // Make the handler available for Remote Procedure Call by AWS Lambda 132 | lambda.Start(Handler) 133 | } 134 | ``` 135 | 136 | ## Other frameworks 137 | This package also supports [Negroni](https://github.com/urfave/negroni), [GorillaMux](https://github.com/gorilla/mux), and plain old `HandlerFunc` - take a look at the code in their respective sub-directories. All packages implement the `Proxy` method exactly like our Gin sample above. 138 | 139 | ## Deploying the sample 140 | We have included a [SAM template](https://github.com/awslabs/serverless-application-model) with our sample application. You can use the [AWS CLI](https://aws.amazon.com/cli/) to quickly deploy the application in your AWS account. 141 | 142 | First, build the sample application by running `make` from the `aws-lambda-go-api-proxy` directory. 143 | 144 | ```bash 145 | $ cd aws-lambda-go-api-proxy 146 | $ make 147 | ``` 148 | 149 | The `make` process should generate a `main.zip` file in the sample folder. You can now use the AWS CLI to prepare the deployment for AWS Lambda and Amazon API Gateway. 150 | 151 | ```bash 152 | $ cd sample 153 | $ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket YOUR_DEPLOYMENT_BUCKET 154 | $ aws cloudformation deploy --template-file output-sam.yaml --stack-name YOUR_STACK_NAME --capabilities CAPABILITY_IAM 155 | ``` 156 | 157 | Using the CloudFormation console, you can find the URL for the newly created API endpoint in the `Outputs` tab of the sample stack - it looks sample like this: `https://xxxxxxxxx.execute-api.xx-xxxx-x.amazonaws.com/Prod/pets`. Open a browser window and try to call the URL. 158 | 159 | ## API Gateway context and stage variables 160 | ~~The `RequestAccessor` object, and therefore `GinLambda`, automatically marshals the API Gateway request context and stage variables objects and stores them in custom headers in the request: `X-GinLambda-ApiGw-Context` and `X-GinLambda-ApiGw-StageVars`. While you could manually unmarshal the json content into the `events.APIGatewayProxyRequestContext` and `map[string]string` objects, the library exports two utility methods to give you easy access to the data.~~ 161 | 162 | The gateway context, stage variables and lambda runtime variables are automatically populate to the context. 163 | 164 | ```go 165 | // the methods are available in your instance of the GinLambda 166 | // object and receive the context 167 | apiGwContext := ginLambda.GetAPIGatewayContextFromContext(ctx) 168 | apiGwStageVars := ginLambda.GetStageVarsFromContext(ctx) 169 | runtimeContext := ginLambda.GetRuntimeContextFromContext(ctx) 170 | 171 | // you can access the properties of the context directly 172 | log.Println(apiGwContext.RequestID) 173 | log.Println(apiGwContext.Stage) 174 | log.Println(runtimeContext.InvokedFunctionArn) 175 | 176 | 177 | // stage variables are stored in a map[string]string 178 | stageVarValue := apiGwStageVars["MyStageVar"] 179 | ``` 180 | 181 | ## Supporting other frameworks 182 | The `aws-lambda-go-api-proxy`, alongside the various adapters, declares a `core` package. The `core` package, contains utility methods and interfaces to translate API Gateway proxy events into Go's default `http.Request` and `http.ResponseWriter` objects. 183 | 184 | You can see that the [`ginlambda.go`](gin/adapter.go) file extends the `RequestAccessor` struct defined in the [`request.go`](core/request.go) file. `RequestAccessor` gives you access to the `ProxyEventToHTTPRequest()` method. 185 | 186 | The `GinLambda` object is initialized with an instance of `gin.Engine`. `gin.Engine` implements methods defined in the `http.Handler` interface. 187 | 188 | The `Proxy` method of the `GinLambda` object simply receives the `events.APIGatewayProxyRequest` object and uses the `ProxyEventToHTTPRequest()` method to convert it into an `http.Request` object. Next, it creates a new `ProxyResponseWriter` object (defined in the [`response.go`](core/response.go)) file and passes both request and response writer to the `ServeHTTP` method of the `gin.Engine`. 189 | 190 | The `ProxyResponseWriter` exports a method called `GetProxyResponse()` to generate an `events.APIGatewayProxyResponse` object from the data written to the response writer. 191 | 192 | Support for frameworks other than Gin can rely on the same methods from the `core` package and swap the `gin.Engine` object for the relevant framework's object. 193 | 194 | ## License 195 | 196 | This library is licensed under the Apache 2.0 License. 197 | -------------------------------------------------------------------------------- /chi/adapter.go: -------------------------------------------------------------------------------- 1 | // Package chiadapter add Chi support for the aws-severless-go-api library. 2 | // Uses the core package behind the scenes and exposes the New method to 3 | // get a new instance and Proxy method to send request to the Chi mux. 4 | package chiadapter 5 | 6 | import ( 7 | "context" 8 | "net/http" 9 | 10 | "github.com/aws/aws-lambda-go/events" 11 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 12 | "github.com/go-chi/chi/v5" 13 | ) 14 | 15 | // ChiLambda makes it easy to send API Gateway proxy events to a Chi 16 | // Mux. The library transforms the proxy event into an HTTP request and then 17 | // creates a proxy response object from the http.ResponseWriter 18 | type ChiLambda struct { 19 | core.RequestAccessor 20 | 21 | chiMux *chi.Mux 22 | } 23 | 24 | // New creates a new instance of the ChiLambda object. 25 | // Receives an initialized *chi.Mux object - normally created with chi.NewRouter(). 26 | // It returns the initialized instance of the ChiLambda object. 27 | func New(chi *chi.Mux) *ChiLambda { 28 | return &ChiLambda{chiMux: chi} 29 | } 30 | 31 | // Proxy receives an API Gateway proxy event, transforms it into an http.Request 32 | // object, and sends it to the chi.Mux for routing. 33 | // It returns a proxy response object generated from the http.ResponseWriter. 34 | func (g *ChiLambda) Proxy(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 35 | chiRequest, err := g.ProxyEventToHTTPRequest(req) 36 | return g.proxyInternal(chiRequest, err) 37 | } 38 | 39 | // ProxyWithContext receives context and an API Gateway proxy event, 40 | // transforms them into an http.Request object, and sends it to the chi.Mux for routing. 41 | // It returns a proxy response object generated from the http.ResponseWriter. 42 | func (g *ChiLambda) ProxyWithContext(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 43 | chiRequest, err := g.EventToRequestWithContext(ctx, req) 44 | return g.proxyInternal(chiRequest, err) 45 | } 46 | 47 | func (g *ChiLambda) proxyInternal(chiRequest *http.Request, err error) (events.APIGatewayProxyResponse, error) { 48 | 49 | if err != nil { 50 | return core.GatewayTimeout(), core.NewLoggedError("Could not convert proxy event to request: %v", err) 51 | } 52 | 53 | respWriter := core.NewProxyResponseWriter() 54 | g.chiMux.ServeHTTP(http.ResponseWriter(respWriter), chiRequest) 55 | 56 | proxyResponse, err := respWriter.GetProxyResponse() 57 | if err != nil { 58 | return core.GatewayTimeout(), core.NewLoggedError("Error while generating proxy response: %v", err) 59 | } 60 | 61 | return proxyResponse, nil 62 | } 63 | -------------------------------------------------------------------------------- /chi/adapterv2.go: -------------------------------------------------------------------------------- 1 | package chiadapter 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/aws/aws-lambda-go/events" 8 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 9 | "github.com/go-chi/chi/v5" 10 | ) 11 | 12 | // ChiLambdaV2 makes it easy to send API Gateway proxy V2 events to a Chi 13 | // Mux. The library transforms the proxy event into an HTTP request and then 14 | // creates a proxy response object from the http.ResponseWriter 15 | type ChiLambdaV2 struct { 16 | core.RequestAccessorV2 17 | 18 | chiMux *chi.Mux 19 | } 20 | 21 | // New creates a new instance of the ChiLambdaV2 object. 22 | // Receives an initialized *chi.Mux object - normally created with chi.NewRouter(). 23 | // It returns the initialized instance of the ChiLambdaV2 object. 24 | func NewV2(chi *chi.Mux) *ChiLambdaV2 { 25 | return &ChiLambdaV2{chiMux: chi} 26 | } 27 | 28 | // Proxy receives an API Gateway proxy V2 event, transforms it into an http.Request 29 | // object, and sends it to the chi.Mux for routing. 30 | // It returns a proxy response object generated from the http.ResponseWriter. 31 | func (g *ChiLambdaV2) Proxy(req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 32 | chiRequest, err := g.ProxyEventToHTTPRequest(req) 33 | return g.proxyInternal(chiRequest, err) 34 | } 35 | 36 | // ProxyWithContext receives context and an API Gateway proxy V2 event, 37 | // transforms them into an http.Request object, and sends it to the chi.Mux for routing. 38 | // It returns a proxy response object generated from the http.ResponseWriter. 39 | func (g *ChiLambdaV2) ProxyWithContextV2(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 40 | chiRequest, err := g.EventToRequestWithContext(ctx, req) 41 | return g.proxyInternal(chiRequest, err) 42 | } 43 | 44 | func (g *ChiLambdaV2) proxyInternal(chiRequest *http.Request, err error) (events.APIGatewayV2HTTPResponse, error) { 45 | 46 | if err != nil { 47 | return core.GatewayTimeoutV2(), core.NewLoggedError("Could not convert proxy event to request: %v", err) 48 | } 49 | 50 | respWriter := core.NewProxyResponseWriterV2() 51 | g.chiMux.ServeHTTP(http.ResponseWriter(respWriter), chiRequest) 52 | 53 | proxyResponse, err := respWriter.GetProxyResponse() 54 | if err != nil { 55 | return core.GatewayTimeoutV2(), core.NewLoggedError("Error while generating proxy response: %v", err) 56 | } 57 | 58 | return proxyResponse, nil 59 | } 60 | -------------------------------------------------------------------------------- /chi/chi_suite_test.go: -------------------------------------------------------------------------------- 1 | package chiadapter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestChi(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Chi Suite") 13 | } 14 | -------------------------------------------------------------------------------- /chi/chilambda_test.go: -------------------------------------------------------------------------------- 1 | package chiadapter_test 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/aws/aws-lambda-go/events" 9 | chiadapter "github.com/awslabs/aws-lambda-go-api-proxy/chi" 10 | "github.com/go-chi/chi/v5" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("ChiLambda tests", func() { 17 | Context("Simple ping request", func() { 18 | It("Proxies the event correctly", func() { 19 | log.Println("Starting test") 20 | 21 | r := chi.NewRouter() 22 | r.Get("/ping", func(w http.ResponseWriter, r *http.Request) { 23 | w.Write([]byte("pong")) 24 | }) 25 | 26 | adapter := chiadapter.New(r) 27 | 28 | req := events.APIGatewayProxyRequest{ 29 | Path: "/ping", 30 | HTTPMethod: "GET", 31 | } 32 | 33 | resp, err := adapter.ProxyWithContext(context.Background(), req) 34 | 35 | Expect(err).To(BeNil()) 36 | Expect(resp.StatusCode).To(Equal(200)) 37 | 38 | resp, err = adapter.Proxy(req) 39 | Expect(err).To(BeNil()) 40 | Expect(resp.StatusCode).To(Equal(200)) 41 | }) 42 | }) 43 | }) 44 | 45 | var _ = Describe("ChiLambdaV2 tests", func() { 46 | Context("Simple ping request", func() { 47 | It("Proxies the event correctly", func() { 48 | log.Println("Starting test") 49 | 50 | r := chi.NewRouter() 51 | r.Get("/ping", func(w http.ResponseWriter, r *http.Request) { 52 | w.Write([]byte("pong")) 53 | }) 54 | 55 | adapter := chiadapter.NewV2(r) 56 | 57 | req := events.APIGatewayV2HTTPRequest{ 58 | RequestContext: events.APIGatewayV2HTTPRequestContext{ 59 | HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{ 60 | Method: "GET", 61 | Path: "/ping", 62 | }, 63 | }, 64 | } 65 | 66 | resp, err := adapter.ProxyWithContextV2(context.Background(), req) 67 | 68 | Expect(err).To(BeNil()) 69 | Expect(resp.StatusCode).To(Equal(200)) 70 | 71 | resp, err = adapter.Proxy(req) 72 | Expect(err).To(BeNil()) 73 | Expect(resp.StatusCode).To(Equal(200)) 74 | }) 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /core/core_suite_test.go: -------------------------------------------------------------------------------- 1 | package core_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestCore(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Core Suite") 13 | } 14 | -------------------------------------------------------------------------------- /core/request.go: -------------------------------------------------------------------------------- 1 | // Package core provides utility methods that help convert proxy events 2 | // into an http.Request and http.ResponseWriter 3 | package core 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "encoding/base64" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "log" 13 | "net/http" 14 | "net/url" 15 | "os" 16 | "strings" 17 | 18 | "github.com/aws/aws-lambda-go/events" 19 | "github.com/aws/aws-lambda-go/lambdacontext" 20 | ) 21 | 22 | const ( 23 | // CustomHostVariable is the name of the environment variable that contains 24 | // the custom hostname for the request. If this variable is not set the framework 25 | // reverts to `RequestContext.DomainName`. The value for a custom host should 26 | // include a protocol: http://my-custom.host.com 27 | CustomHostVariable = "GO_API_HOST" 28 | 29 | // APIGwContextHeader is the custom header key used to store the 30 | // API Gateway context. To access the Context properties use the 31 | // GetAPIGatewayContext method of the RequestAccessor object. 32 | APIGwContextHeader = "X-GoLambdaProxy-ApiGw-Context" 33 | 34 | // APIGwStageVarsHeader is the custom header key used to store the 35 | // API Gateway stage variables. To access the stage variable values 36 | // use the GetAPIGatewayStageVars method of the RequestAccessor object. 37 | APIGwStageVarsHeader = "X-GoLambdaProxy-ApiGw-StageVars" 38 | ) 39 | 40 | // RequestAccessor objects give access to custom API Gateway properties 41 | // in the request. 42 | type RequestAccessor struct { 43 | stripBasePath string 44 | } 45 | 46 | // GetAPIGatewayContext extracts the API Gateway context object from a 47 | // request's custom header. 48 | // Returns a populated events.APIGatewayProxyRequestContext object from 49 | // the request. 50 | func (r *RequestAccessor) GetAPIGatewayContext(req *http.Request) (events.APIGatewayProxyRequestContext, error) { 51 | if req.Header.Get(APIGwContextHeader) == "" { 52 | return events.APIGatewayProxyRequestContext{}, errors.New("No context header in request") 53 | } 54 | context := events.APIGatewayProxyRequestContext{} 55 | err := json.Unmarshal([]byte(req.Header.Get(APIGwContextHeader)), &context) 56 | if err != nil { 57 | log.Println("Error while unmarshalling context") 58 | log.Println(err) 59 | return events.APIGatewayProxyRequestContext{}, err 60 | } 61 | return context, nil 62 | } 63 | 64 | // GetAPIGatewayStageVars extracts the API Gateway stage variables from a 65 | // request's custom header. 66 | // Returns a map[string]string of the stage variables and their values from 67 | // the request. 68 | func (r *RequestAccessor) GetAPIGatewayStageVars(req *http.Request) (map[string]string, error) { 69 | stageVars := make(map[string]string) 70 | if req.Header.Get(APIGwStageVarsHeader) == "" { 71 | return stageVars, errors.New("No stage vars header in request") 72 | } 73 | err := json.Unmarshal([]byte(req.Header.Get(APIGwStageVarsHeader)), &stageVars) 74 | if err != nil { 75 | log.Println("Error while unmarshalling stage variables") 76 | log.Println(err) 77 | return stageVars, err 78 | } 79 | return stageVars, nil 80 | } 81 | 82 | // StripBasePath instructs the RequestAccessor object that the given base 83 | // path should be removed from the request path before sending it to the 84 | // framework for routing. This is used when API Gateway is configured with 85 | // base path mappings in custom domain names. 86 | func (r *RequestAccessor) StripBasePath(basePath string) string { 87 | if strings.Trim(basePath, " ") == "" { 88 | r.stripBasePath = "" 89 | return "" 90 | } 91 | 92 | newBasePath := basePath 93 | if !strings.HasPrefix(newBasePath, "/") { 94 | newBasePath = "/" + newBasePath 95 | } 96 | 97 | if strings.HasSuffix(newBasePath, "/") { 98 | newBasePath = newBasePath[:len(newBasePath)-1] 99 | } 100 | 101 | r.stripBasePath = newBasePath 102 | 103 | return newBasePath 104 | } 105 | 106 | // ProxyEventToHTTPRequest converts an API Gateway proxy event into a http.Request object. 107 | // Returns the populated http request with additional two custom headers for the stage variables and API Gateway context. 108 | // To access these properties use the GetAPIGatewayStageVars and GetAPIGatewayContext method of the RequestAccessor object. 109 | func (r *RequestAccessor) ProxyEventToHTTPRequest(req events.APIGatewayProxyRequest) (*http.Request, error) { 110 | httpRequest, err := r.EventToRequest(req) 111 | if err != nil { 112 | log.Println(err) 113 | return nil, err 114 | } 115 | return addToHeader(httpRequest, req) 116 | } 117 | 118 | // EventToRequestWithContext converts an API Gateway proxy event and context into an http.Request object. 119 | // Returns the populated http request with lambda context, stage variables and APIGatewayProxyRequestContext as part of its context. 120 | // Access those using GetAPIGatewayContextFromContext, GetStageVarsFromContext and GetRuntimeContextFromContext functions in this package. 121 | func (r *RequestAccessor) EventToRequestWithContext(ctx context.Context, req events.APIGatewayProxyRequest) (*http.Request, error) { 122 | httpRequest, err := r.EventToRequest(req) 123 | if err != nil { 124 | log.Println(err) 125 | return nil, err 126 | } 127 | return addToContext(ctx, httpRequest, req), nil 128 | } 129 | 130 | // EventToRequest converts an API Gateway proxy event into an http.Request object. 131 | // Returns the populated request maintaining headers 132 | func (r *RequestAccessor) EventToRequest(req events.APIGatewayProxyRequest) (*http.Request, error) { 133 | decodedBody := []byte(req.Body) 134 | if req.IsBase64Encoded { 135 | base64Body, err := base64.StdEncoding.DecodeString(req.Body) 136 | if err != nil { 137 | return nil, err 138 | } 139 | decodedBody = base64Body 140 | } 141 | 142 | path := req.Path 143 | if r.stripBasePath != "" && len(r.stripBasePath) > 1 { 144 | if strings.HasPrefix(path, r.stripBasePath) { 145 | path = strings.Replace(path, r.stripBasePath, "", 1) 146 | } 147 | } 148 | if !strings.HasPrefix(path, "/") { 149 | path = "/" + path 150 | } 151 | serverAddress := "https://" + req.RequestContext.DomainName 152 | if customAddress, ok := os.LookupEnv(CustomHostVariable); ok { 153 | serverAddress = customAddress 154 | } 155 | path = serverAddress + path 156 | 157 | if len(req.MultiValueQueryStringParameters) > 0 { 158 | queryString := "" 159 | for q, l := range req.MultiValueQueryStringParameters { 160 | for _, v := range l { 161 | if queryString != "" { 162 | queryString += "&" 163 | } 164 | queryString += url.QueryEscape(q) + "=" + url.QueryEscape(v) 165 | } 166 | } 167 | path += "?" + queryString 168 | } else if len(req.QueryStringParameters) > 0 { 169 | // Support `QueryStringParameters` for backward compatibility. 170 | // https://github.com/awslabs/aws-lambda-go-api-proxy/issues/37 171 | queryString := "" 172 | for q := range req.QueryStringParameters { 173 | if queryString != "" { 174 | queryString += "&" 175 | } 176 | queryString += url.QueryEscape(q) + "=" + url.QueryEscape(req.QueryStringParameters[q]) 177 | } 178 | path += "?" + queryString 179 | } 180 | 181 | httpRequest, err := http.NewRequest( 182 | strings.ToUpper(req.HTTPMethod), 183 | path, 184 | bytes.NewReader(decodedBody), 185 | ) 186 | 187 | if err != nil { 188 | fmt.Printf("Could not convert request %s:%s to http.Request\n", req.HTTPMethod, req.Path) 189 | log.Println(err) 190 | return nil, err 191 | } 192 | 193 | httpRequest.RemoteAddr = req.RequestContext.Identity.SourceIP 194 | 195 | if req.MultiValueHeaders != nil { 196 | for k, values := range req.MultiValueHeaders { 197 | for _, value := range values { 198 | httpRequest.Header.Add(k, value) 199 | } 200 | } 201 | } else { 202 | for h := range req.Headers { 203 | httpRequest.Header.Add(h, req.Headers[h]) 204 | } 205 | } 206 | 207 | httpRequest.RequestURI = httpRequest.URL.RequestURI() 208 | 209 | return httpRequest, nil 210 | } 211 | 212 | func addToHeader(req *http.Request, apiGwRequest events.APIGatewayProxyRequest) (*http.Request, error) { 213 | stageVars, err := json.Marshal(apiGwRequest.StageVariables) 214 | if err != nil { 215 | log.Println("Could not marshal stage variables for custom header") 216 | return nil, err 217 | } 218 | req.Header.Set(APIGwStageVarsHeader, string(stageVars)) 219 | apiGwContext, err := json.Marshal(apiGwRequest.RequestContext) 220 | if err != nil { 221 | log.Println("Could not Marshal API GW context for custom header") 222 | return req, err 223 | } 224 | req.Header.Set(APIGwContextHeader, string(apiGwContext)) 225 | return req, nil 226 | } 227 | 228 | func addToContext(ctx context.Context, req *http.Request, apiGwRequest events.APIGatewayProxyRequest) *http.Request { 229 | lc, _ := lambdacontext.FromContext(ctx) 230 | rc := requestContext{lambdaContext: lc, gatewayProxyContext: apiGwRequest.RequestContext, stageVars: apiGwRequest.StageVariables} 231 | ctx = context.WithValue(ctx, ctxKey{}, rc) 232 | return req.WithContext(ctx) 233 | } 234 | 235 | // GetAPIGatewayContextFromContext retrieve APIGatewayProxyRequestContext from context.Context 236 | func GetAPIGatewayContextFromContext(ctx context.Context) (events.APIGatewayProxyRequestContext, bool) { 237 | v, ok := ctx.Value(ctxKey{}).(requestContext) 238 | return v.gatewayProxyContext, ok 239 | } 240 | 241 | // GetRuntimeContextFromContext retrieve Lambda Runtime Context from context.Context 242 | func GetRuntimeContextFromContext(ctx context.Context) (*lambdacontext.LambdaContext, bool) { 243 | v, ok := ctx.Value(ctxKey{}).(requestContext) 244 | return v.lambdaContext, ok 245 | } 246 | 247 | // GetStageVarsFromContext retrieve stage variables from context 248 | func GetStageVarsFromContext(ctx context.Context) (map[string]string, bool) { 249 | v, ok := ctx.Value(ctxKey{}).(requestContext) 250 | return v.stageVars, ok 251 | } 252 | 253 | type ctxKey struct{} 254 | 255 | type requestContext struct { 256 | lambdaContext *lambdacontext.LambdaContext 257 | gatewayProxyContext events.APIGatewayProxyRequestContext 258 | stageVars map[string]string 259 | } 260 | -------------------------------------------------------------------------------- /core/requestALB.go: -------------------------------------------------------------------------------- 1 | // Package core provides utility methods that help convert ALB events 2 | // into an http.Request and http.ResponseWriter 3 | package core 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "encoding/base64" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "log" 13 | "net/http" 14 | "net/url" 15 | "strings" 16 | 17 | "github.com/aws/aws-lambda-go/events" 18 | "github.com/aws/aws-lambda-go/lambdacontext" 19 | ) 20 | 21 | const ( 22 | // ALBContextHeader is the custom header key used to store the 23 | // ALB ELB context. To access the Context properties use the 24 | // GetALBContext method of the RequestAccessorALB object. 25 | ALBContextHeader = "X-GoLambdaProxy-ALB-Context" 26 | ) 27 | 28 | // RequestAccessorALB objects give access to custom ALB Target Group properties 29 | // in the request. 30 | type RequestAccessorALB struct { 31 | stripBasePath string 32 | } 33 | 34 | // GetALBContext extracts the ALB context object from a request's custom header. 35 | // Returns a populated events.ALBTargetGroupRequestContext object from the request. 36 | func (r *RequestAccessorALB) GetContextALB(req *http.Request) (events.ALBTargetGroupRequestContext, error) { 37 | if req.Header.Get(ALBContextHeader) == "" { 38 | return events.ALBTargetGroupRequestContext{}, errors.New("no context header in request") 39 | } 40 | context := events.ALBTargetGroupRequestContext{} 41 | err := json.Unmarshal([]byte(req.Header.Get(ALBContextHeader)), &context) 42 | if err != nil { 43 | log.Println("Error while unmarshalling context") 44 | log.Println(err) 45 | return events.ALBTargetGroupRequestContext{}, err 46 | } 47 | return context, nil 48 | } 49 | 50 | // StripBasePath instructs the RequestAccessor object that the given base 51 | // path should be removed from the request path before sending it to the 52 | // framework for routing. This is used when API Gateway is configured with 53 | // base path mappings in custom domain names. 54 | func (r *RequestAccessorALB) StripBasePath(basePath string) string { 55 | if strings.Trim(basePath, " ") == "" { 56 | r.stripBasePath = "" 57 | return "" 58 | } 59 | 60 | newBasePath := basePath 61 | if !strings.HasPrefix(newBasePath, "/") { 62 | newBasePath = "/" + newBasePath 63 | } 64 | 65 | if strings.HasSuffix(newBasePath, "/") { 66 | newBasePath = newBasePath[:len(newBasePath)-1] 67 | } 68 | 69 | r.stripBasePath = newBasePath 70 | 71 | return newBasePath 72 | } 73 | 74 | // ProxyEventToHTTPRequest converts an ALB Target Group Request event into a http.Request object. 75 | // Returns the populated http request with additional custom header for the ALB context. 76 | // To access these properties use the GetALBContext method of the RequestAccessorALB object. 77 | func (r *RequestAccessorALB) ProxyEventToHTTPRequest(req events.ALBTargetGroupRequest) (*http.Request, error) { 78 | httpRequest, err := r.EventToRequest(req) 79 | if err != nil { 80 | log.Println(err) 81 | return nil, err 82 | } 83 | return addToHeaderALB(httpRequest, req) 84 | } 85 | 86 | // EventToRequestWithContext converts an ALB Target Group Request event and context into an http.Request object. 87 | // Returns the populated http request with lambda context, ALB TargetGroup RequestContext as part of its context. 88 | func (r *RequestAccessorALB) EventToRequestWithContext(ctx context.Context, req events.ALBTargetGroupRequest) (*http.Request, error) { 89 | httpRequest, err := r.EventToRequest(req) 90 | if err != nil { 91 | log.Println(err) 92 | return nil, err 93 | } 94 | return addToContextALB(ctx, httpRequest, req), nil 95 | } 96 | 97 | // EventToRequest converts an ALB TargetGroup event into an http.Request object. 98 | // Returns the populated request maintaining headers 99 | func (r *RequestAccessorALB) EventToRequest(req events.ALBTargetGroupRequest) (*http.Request, error) { 100 | decodedBody := []byte(req.Body) 101 | if req.IsBase64Encoded { 102 | base64Body, err := base64.StdEncoding.DecodeString(req.Body) 103 | if err != nil { 104 | return nil, err 105 | } 106 | decodedBody = base64Body 107 | } 108 | 109 | path := req.Path 110 | if r.stripBasePath != "" && len(r.stripBasePath) > 1 { 111 | if strings.HasPrefix(path, r.stripBasePath) { 112 | path = strings.Replace(path, r.stripBasePath, "", 1) 113 | } 114 | } 115 | if !strings.HasPrefix(path, "/") { 116 | path = "/" + path 117 | } 118 | serverAddress := "https://" + req.Headers["host"] 119 | // if customAddress, ok := os.LookupEnv(CustomHostVariable); ok { 120 | // serverAddress = customAddress 121 | // } 122 | path = serverAddress + path 123 | 124 | if len(req.MultiValueQueryStringParameters) > 0 { 125 | queryString := "" 126 | for q, l := range req.MultiValueQueryStringParameters { 127 | for _, v := range l { 128 | if queryString != "" { 129 | queryString += "&" 130 | } 131 | queryString += url.QueryEscape(q) + "=" + url.QueryEscape(v) 132 | } 133 | } 134 | path += "?" + queryString 135 | } else if len(req.QueryStringParameters) > 0 { 136 | // Support `QueryStringParameters` for backward compatibility. 137 | // https://github.com/awslabs/aws-lambda-go-api-proxy/issues/37 138 | queryString := "" 139 | for q := range req.QueryStringParameters { 140 | if queryString != "" { 141 | queryString += "&" 142 | } 143 | queryString += url.QueryEscape(q) + "=" + url.QueryEscape(req.QueryStringParameters[q]) 144 | } 145 | path += "?" + queryString 146 | } 147 | 148 | httpRequest, err := http.NewRequest( 149 | strings.ToUpper(req.HTTPMethod), 150 | path, 151 | bytes.NewReader(decodedBody), 152 | ) 153 | 154 | if err != nil { 155 | fmt.Printf("Could not convert request %s:%s to http.Request\n", req.HTTPMethod, req.Path) 156 | log.Println(err) 157 | return nil, err 158 | } 159 | 160 | if req.MultiValueHeaders != nil { 161 | for k, values := range req.MultiValueHeaders { 162 | for _, value := range values { 163 | httpRequest.Header.Add(k, value) 164 | } 165 | } 166 | } else { 167 | for h := range req.Headers { 168 | httpRequest.Header.Add(h, req.Headers[h]) 169 | } 170 | } 171 | 172 | httpRequest.RequestURI = httpRequest.URL.RequestURI() 173 | 174 | return httpRequest, nil 175 | } 176 | 177 | func addToHeaderALB(req *http.Request, albRequest events.ALBTargetGroupRequest) (*http.Request, error) { 178 | albContext, err := json.Marshal(albRequest.RequestContext) 179 | if err != nil { 180 | log.Println("Could not Marshal ALB context for custom header") 181 | return req, err 182 | } 183 | req.Header.Set(ALBContextHeader, string(albContext)) 184 | return req, nil 185 | } 186 | 187 | // adds context data to http request so we can pass 188 | func addToContextALB(ctx context.Context, req *http.Request, albRequest events.ALBTargetGroupRequest) *http.Request { 189 | lc, _ := lambdacontext.FromContext(ctx) 190 | rc := requestContextALB{lambdaContext: lc, albContext: albRequest.RequestContext} 191 | ctx = context.WithValue(ctx, ctxKey{}, rc) 192 | return req.WithContext(ctx) 193 | } 194 | 195 | // GetALBTargetGroupRequestFromContext retrieve ALBTargetGroupt from context.Context 196 | func GetTargetGroupRequetFromContextALB(ctx context.Context) (events.ALBTargetGroupRequestContext, bool) { 197 | v, ok := ctx.Value(ctxKey{}).(requestContextALB) 198 | return v.albContext, ok 199 | } 200 | 201 | // GetRuntimeContextFromContext retrieve Lambda Runtime Context from context.Context 202 | func GetRuntimeContextFromContextALB(ctx context.Context) (*lambdacontext.LambdaContext, bool) { 203 | v, ok := ctx.Value(ctxKey{}).(requestContextALB) 204 | return v.lambdaContext, ok 205 | } 206 | 207 | type requestContextALB struct { 208 | lambdaContext *lambdacontext.LambdaContext 209 | albContext events.ALBTargetGroupRequestContext 210 | } 211 | -------------------------------------------------------------------------------- /core/requestALB_test.go: -------------------------------------------------------------------------------- 1 | package core_test 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "math/rand" 7 | "strings" 8 | 9 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 10 | 11 | "github.com/aws/aws-lambda-go/events" 12 | "github.com/aws/aws-lambda-go/lambdacontext" 13 | 14 | . "github.com/onsi/ginkgo" 15 | . "github.com/onsi/gomega" 16 | ) 17 | 18 | var _ = Describe("RequestAccessorALB tests", func() { 19 | Context("ALB event conversion", func() { 20 | accessor := core.RequestAccessorALB{} 21 | qs := make(map[string]string) 22 | mvh := make(map[string][]string) 23 | mvqs := make(map[string][]string) 24 | hdr := make(map[string]string) 25 | qs["UniqueId"] = "12345" 26 | mvh["accept"] = []string{"test", "one"} 27 | mvh["connection"] = []string{"keep-alive"} 28 | mvh["host"] = []string{"lambda-test-alb-1234567.us-east-1.elb.amazonaws.com"} 29 | hdr["header1"] = "Testhdr1" 30 | hdr["header2"] = "Testhdr2" 31 | //multivalue querystrings 32 | mvqs["k1"] = []string{"t1"} 33 | mvqs["k2"] = []string{"t2"} 34 | bdy := "Test BODY" 35 | basicRequest := getALBProxyRequest("/hello", "GET", getALBRequestContext(), false, hdr, bdy, qs, mvh, nil) 36 | 37 | It("Correctly converts a basic event", func() { 38 | httpReq, err := accessor.EventToRequestWithContext(context.Background(), basicRequest) 39 | Expect(err).To(BeNil()) 40 | Expect("/hello").To(Equal(httpReq.URL.Path)) 41 | Expect("/hello?UniqueId=12345").To(Equal(httpReq.RequestURI)) 42 | Expect("GET").To(Equal(httpReq.Method)) 43 | headers := basicRequest.Headers 44 | Expect(2).To(Equal(len(headers))) 45 | mvhs := basicRequest.MultiValueHeaders 46 | Expect(3).To(Equal(len(mvhs))) 47 | mvqs := basicRequest.MultiValueQueryStringParameters 48 | Expect(0).To(Equal(len(mvqs))) 49 | 50 | }) 51 | 52 | binaryBody := make([]byte, 256) 53 | _, err := rand.Read(binaryBody) 54 | if err != nil { 55 | Fail("Could not generate random binary body") 56 | } 57 | 58 | encodedBody := base64.StdEncoding.EncodeToString(binaryBody) 59 | 60 | binaryRequest := getALBProxyRequest("/hello", "POST", getALBRequestContext(), true, hdr, bdy, qs, mvh, nil) 61 | binaryRequest.Body = encodedBody 62 | binaryRequest.IsBase64Encoded = true 63 | 64 | It("Decodes a base64 encoded body", func() { 65 | httpReq, err := accessor.EventToRequestWithContext(context.Background(), binaryRequest) 66 | Expect(err).To(BeNil()) 67 | Expect("/hello").To(Equal(httpReq.URL.Path)) 68 | Expect("/hello?UniqueId=12345").To(Equal(httpReq.RequestURI)) 69 | Expect("POST").To(Equal(httpReq.Method)) 70 | 71 | Expect(err).To(BeNil()) 72 | 73 | }) 74 | 75 | mqsRequest := getALBProxyRequest("/hello", "GET", getALBRequestContext(), false, hdr, bdy, qs, mvh, nil) 76 | mqsRequest.QueryStringParameters = map[string]string{ 77 | "hello": "1", 78 | "world": "2", 79 | } 80 | It("Populates multiple value query string correctly", func() { 81 | httpReq, err := accessor.EventToRequestWithContext(context.Background(), mqsRequest) 82 | Expect(err).To(BeNil()) 83 | Expect("/hello").To(Equal(httpReq.URL.Path)) 84 | Expect(httpReq.RequestURI).To(ContainSubstring("hello=1")) 85 | Expect(httpReq.RequestURI).To(ContainSubstring("world=2")) 86 | Expect("GET").To(Equal(httpReq.Method)) 87 | 88 | query := httpReq.URL.Query() 89 | Expect(2).To(Equal(len(query))) 90 | Expect(query["hello"]).ToNot(BeNil()) 91 | Expect(query["world"]).ToNot(BeNil()) 92 | Expect(1).To(Equal(len(query["hello"]))) 93 | Expect("1").To(Equal(query["hello"][0])) 94 | Expect("2").To(Equal(query["world"][0])) 95 | 96 | }) 97 | 98 | qsRequest := getALBProxyRequest("/hello", "GET", getALBRequestContext(), false, hdr, bdy, qs, mvh, nil) 99 | qsRequest.QueryStringParameters = map[string]string{ 100 | "hello": "1", 101 | "world": "2", 102 | } 103 | It("Populates query string correctly", func() { 104 | httpReq, err := accessor.EventToRequestWithContext(context.Background(), qsRequest) 105 | Expect(err).To(BeNil()) 106 | Expect("/hello").To(Equal(httpReq.URL.Path)) 107 | Expect(httpReq.RequestURI).To(ContainSubstring("hello=1")) 108 | Expect(httpReq.RequestURI).To(ContainSubstring("world=2")) 109 | Expect("GET").To(Equal(httpReq.Method)) 110 | 111 | query := httpReq.URL.Query() 112 | Expect(2).To(Equal(len(query))) 113 | Expect(query["hello"]).ToNot(BeNil()) 114 | Expect(query["world"]).ToNot(BeNil()) 115 | Expect(1).To(Equal(len(query["hello"]))) 116 | Expect(1).To(Equal(len(query["world"]))) 117 | Expect("1").To(Equal(query["hello"][0])) 118 | Expect("2").To(Equal(query["world"][0])) 119 | }) 120 | 121 | // If multivaluehaders are set then it only passes the multivalue headers to the http.Request 122 | mvhRequest := getALBProxyRequest("/hello", "GET", getALBRequestContext(), false, hdr, bdy, qs, nil, mvqs) 123 | mvhRequest.MultiValueHeaders = map[string][]string{ 124 | "accept": {"test", "one"}, 125 | "connection": {"keep-alive"}, 126 | "host": {"lambda-test-alb-1234567.us-east-1.elb.amazonaws.com"}, 127 | } 128 | It("Populates multiple value headers correctly", func() { 129 | httpReq, err := accessor.EventToRequestWithContext(context.Background(), mvhRequest) 130 | Expect(err).To(BeNil()) 131 | Expect("/hello").To(Equal(httpReq.URL.Path)) 132 | Expect("GET").To(Equal(httpReq.Method)) 133 | 134 | headers := httpReq.Header 135 | Expect(3).To(Equal(len(headers))) 136 | 137 | for k, value := range headers { 138 | Expect(value).To(Equal(mvhRequest.MultiValueHeaders[strings.ToLower(k)])) 139 | } 140 | 141 | }) 142 | // If multivaluehaders are set then it only passes the multivalue headers to the http.Request 143 | svhRequest := getALBProxyRequest("/hello", "GET", getALBRequestContext(), false, hdr, bdy, qs, mvh, mvqs) 144 | svhRequest.Headers = map[string]string{ 145 | "header1": "Testhdr1", 146 | "header2": "Testhdr2"} 147 | 148 | It("Populates single value headers correctly", func() { 149 | httpReq, err := accessor.EventToRequestWithContext(context.Background(), svhRequest) 150 | Expect(err).To(BeNil()) 151 | Expect("/hello").To(Equal(httpReq.URL.Path)) 152 | Expect("GET").To(Equal(httpReq.Method)) 153 | 154 | headers := httpReq.Header 155 | Expect(3).To(Equal(len(headers))) 156 | 157 | for k, value := range headers { 158 | Expect(value).To(Equal(mvhRequest.MultiValueHeaders[strings.ToLower(k)])) 159 | } 160 | 161 | }) 162 | 163 | basePathRequest := getALBProxyRequest("/app1/orders", "GET", getALBRequestContext(), false, hdr, bdy, qs, mvh, nil) 164 | 165 | It("Stips the base path correct", func() { 166 | accessor.StripBasePath("app1") 167 | httpReq, err := accessor.EventToRequestWithContext(context.Background(), basePathRequest) 168 | 169 | Expect(err).To(BeNil()) 170 | Expect("/orders").To(Equal(httpReq.URL.Path)) 171 | Expect("/orders?UniqueId=12345").To(Equal(httpReq.RequestURI)) 172 | }) 173 | 174 | contextRequest := getALBProxyRequest("orders", "GET", getALBRequestContext(), false, hdr, bdy, qs, mvh, mvqs) 175 | contextRequest.RequestContext = getALBRequestContext() 176 | 177 | It("Populates context header correctly", func() { 178 | // calling old method to verify reverse compatibility 179 | httpReq, err := accessor.ProxyEventToHTTPRequest(contextRequest) 180 | Expect(err).To(BeNil()) 181 | Expect(4).To(Equal(len(httpReq.Header))) 182 | Expect(httpReq.Header.Get(core.ALBContextHeader)).ToNot(BeNil()) 183 | }) 184 | }) 185 | 186 | Context("StripBasePath tests", func() { 187 | accessor := core.RequestAccessorALB{} 188 | It("Adds prefix slash", func() { 189 | basePath := accessor.StripBasePath("app1") 190 | Expect("/app1").To(Equal(basePath)) 191 | }) 192 | 193 | It("Removes trailing slash", func() { 194 | basePath := accessor.StripBasePath("/app1/") 195 | Expect("/app1").To(Equal(basePath)) 196 | }) 197 | 198 | It("Ignores blank strings", func() { 199 | basePath := accessor.StripBasePath(" ") 200 | Expect("").To(Equal(basePath)) 201 | }) 202 | }) 203 | 204 | Context("Retrieves ALB Target Group Request context", func() { 205 | It("Returns a correctly unmarshalled object", func() { 206 | qs := make(map[string]string) 207 | mvh := make(map[string][]string) 208 | hdr := make(map[string]string) 209 | mvqs := make(map[string][]string) 210 | qs["UniqueId"] = "12345" 211 | mvh["accept"] = []string{"*/*", "/"} 212 | mvh["connection"] = []string{"keep-alive"} 213 | mvh["host"] = []string{"lambda-test-alb-1234567.us-east-1.elb.amazonaws.com"} 214 | mvqs["key1"] = []string{"Test1"} 215 | mvqs["key2"] = []string{"test2"} 216 | hdr["header1"] = "Testhdr1" 217 | bdy := "Test BODY2" 218 | 219 | contextRequest := getALBProxyRequest("/orders", "GET", getALBRequestContext(), false, hdr, bdy, qs, mvh, mvqs) 220 | contextRequest.RequestContext = getALBRequestContext() 221 | 222 | accessor := core.RequestAccessorALB{} 223 | // calling old method to verify reverse compatibility 224 | httpReq, err := accessor.ProxyEventToHTTPRequest(contextRequest) 225 | Expect(err).To(BeNil()) 226 | 227 | headerContext, err := accessor.GetContextALB(httpReq) 228 | Expect(err).To(BeNil()) 229 | Expect(headerContext).ToNot(BeNil()) 230 | Expect("arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/lambda-target/abcdefgh").To(Equal(headerContext.ELB.TargetGroupArn)) 231 | proxyContext, ok := core.GetTargetGroupRequetFromContextALB(httpReq.Context()) 232 | // should fail because using header proxy method 233 | Expect(ok).To(BeFalse()) 234 | 235 | httpReq, err = accessor.EventToRequestWithContext(context.Background(), contextRequest) 236 | Expect(err).To(BeNil()) 237 | proxyContext, ok = core.GetTargetGroupRequetFromContextALB(httpReq.Context()) 238 | Expect(ok).To(BeTrue()) 239 | Expect("arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/lambda-target/abcdefgh").To(Equal(proxyContext.ELB.TargetGroupArn)) 240 | runtimeContext, ok := core.GetRuntimeContextFromContextALB(httpReq.Context()) 241 | Expect(ok).To(BeTrue()) 242 | Expect(runtimeContext).To(BeNil()) 243 | 244 | lambdaContext := lambdacontext.NewContext(context.Background(), &lambdacontext.LambdaContext{AwsRequestID: "abc123"}) 245 | httpReq, err = accessor.EventToRequestWithContext(lambdaContext, contextRequest) 246 | Expect(err).To(BeNil()) 247 | 248 | headerContext, err = accessor.GetContextALB(httpReq) 249 | // should fail as new context method doesn't populate headers 250 | Expect(err).ToNot(BeNil()) 251 | proxyContext, ok = core.GetTargetGroupRequetFromContextALB(httpReq.Context()) 252 | Expect(ok).To(BeTrue()) 253 | Expect("arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/lambda-target/abcdefgh").To(Equal(proxyContext.ELB.TargetGroupArn)) 254 | runtimeContext, ok = core.GetRuntimeContextFromContextALB(httpReq.Context()) 255 | Expect(ok).To(BeTrue()) 256 | Expect(runtimeContext).ToNot(BeNil()) 257 | 258 | }) 259 | }) 260 | }) 261 | 262 | func getALBProxyRequest(path string, method string, requestCtx events.ALBTargetGroupRequestContext, 263 | is64 bool, header map[string]string, body string, qs map[string]string, mvh map[string][]string, mvqsp map[string][]string) events.ALBTargetGroupRequest { 264 | return events.ALBTargetGroupRequest{ 265 | HTTPMethod: method, 266 | Path: path, 267 | QueryStringParameters: qs, 268 | MultiValueQueryStringParameters: mvqsp, 269 | Headers: header, 270 | MultiValueHeaders: mvh, 271 | RequestContext: requestCtx, 272 | IsBase64Encoded: is64, 273 | Body: body, 274 | } 275 | } 276 | 277 | func getALBRequestContext() events.ALBTargetGroupRequestContext { 278 | return events.ALBTargetGroupRequestContext{ 279 | ELB: events.ELBContext{ 280 | TargetGroupArn: "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/lambda-target/abcdefgh", 281 | }, 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /core/request_test.go: -------------------------------------------------------------------------------- 1 | package core_test 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "io/ioutil" 7 | "math/rand" 8 | "os" 9 | "strings" 10 | 11 | "github.com/aws/aws-lambda-go/events" 12 | "github.com/aws/aws-lambda-go/lambdacontext" 13 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 14 | 15 | . "github.com/onsi/ginkgo" 16 | . "github.com/onsi/gomega" 17 | ) 18 | 19 | var _ = Describe("RequestAccessor tests", func() { 20 | Context("event conversion", func() { 21 | accessor := core.RequestAccessor{} 22 | basicRequest := getProxyRequest("/hello", "GET") 23 | It("Correctly converts a basic event", func() { 24 | httpReq, err := accessor.EventToRequestWithContext(context.Background(), basicRequest) 25 | Expect(err).To(BeNil()) 26 | Expect("/hello").To(Equal(httpReq.URL.Path)) 27 | Expect("/hello").To(Equal(httpReq.RequestURI)) 28 | Expect("GET").To(Equal(httpReq.Method)) 29 | }) 30 | 31 | basicRequest = getProxyRequest("/hello", "get") 32 | It("Converts method to uppercase", func() { 33 | // calling old method to verify reverse compatibility 34 | httpReq, err := accessor.ProxyEventToHTTPRequest(basicRequest) 35 | Expect(err).To(BeNil()) 36 | Expect("/hello").To(Equal(httpReq.URL.Path)) 37 | Expect("/hello").To(Equal(httpReq.RequestURI)) 38 | Expect("GET").To(Equal(httpReq.Method)) 39 | }) 40 | 41 | binaryBody := make([]byte, 256) 42 | _, err := rand.Read(binaryBody) 43 | if err != nil { 44 | Fail("Could not generate random binary body") 45 | } 46 | 47 | encodedBody := base64.StdEncoding.EncodeToString(binaryBody) 48 | 49 | binaryRequest := getProxyRequest("/hello", "POST") 50 | binaryRequest.Body = encodedBody 51 | binaryRequest.IsBase64Encoded = true 52 | 53 | It("Decodes a base64 encoded body", func() { 54 | httpReq, err := accessor.EventToRequestWithContext(context.Background(), binaryRequest) 55 | Expect(err).To(BeNil()) 56 | Expect("/hello").To(Equal(httpReq.URL.Path)) 57 | Expect("/hello").To(Equal(httpReq.RequestURI)) 58 | Expect("POST").To(Equal(httpReq.Method)) 59 | 60 | bodyBytes, err := ioutil.ReadAll(httpReq.Body) 61 | 62 | Expect(err).To(BeNil()) 63 | Expect(len(binaryBody)).To(Equal(len(bodyBytes))) 64 | Expect(binaryBody).To(Equal(bodyBytes)) 65 | }) 66 | 67 | mqsRequest := getProxyRequest("/hello", "GET") 68 | mqsRequest.MultiValueQueryStringParameters = map[string][]string{ 69 | "hello": {"1"}, 70 | "world": {"2", "3"}, 71 | } 72 | mqsRequest.QueryStringParameters = map[string]string{ 73 | "hello": "1", 74 | "world": "2", 75 | } 76 | It("Populates multiple value query string correctly", func() { 77 | httpReq, err := accessor.EventToRequestWithContext(context.Background(), mqsRequest) 78 | Expect(err).To(BeNil()) 79 | Expect("/hello").To(Equal(httpReq.URL.Path)) 80 | Expect(httpReq.RequestURI).To(ContainSubstring("hello=1")) 81 | Expect(httpReq.RequestURI).To(ContainSubstring("world=2")) 82 | Expect(httpReq.RequestURI).To(ContainSubstring("world=3")) 83 | Expect("GET").To(Equal(httpReq.Method)) 84 | 85 | query := httpReq.URL.Query() 86 | Expect(2).To(Equal(len(query))) 87 | Expect(query["hello"]).ToNot(BeNil()) 88 | Expect(query["world"]).ToNot(BeNil()) 89 | Expect(1).To(Equal(len(query["hello"]))) 90 | Expect(2).To(Equal(len(query["world"]))) 91 | Expect("1").To(Equal(query["hello"][0])) 92 | Expect("2").To(Equal(query["world"][0])) 93 | Expect("3").To(Equal(query["world"][1])) 94 | }) 95 | 96 | // Support `QueryStringParameters` for backward compatibility. 97 | // https://github.com/awslabs/aws-lambda-go-api-proxy/issues/37 98 | qsRequest := getProxyRequest("/hello", "GET") 99 | qsRequest.QueryStringParameters = map[string]string{ 100 | "hello": "1", 101 | "world": "2", 102 | } 103 | It("Populates query string correctly", func() { 104 | httpReq, err := accessor.EventToRequestWithContext(context.Background(), qsRequest) 105 | Expect(err).To(BeNil()) 106 | Expect("/hello").To(Equal(httpReq.URL.Path)) 107 | Expect(httpReq.RequestURI).To(ContainSubstring("hello=1")) 108 | Expect(httpReq.RequestURI).To(ContainSubstring("world=2")) 109 | Expect("GET").To(Equal(httpReq.Method)) 110 | 111 | query := httpReq.URL.Query() 112 | Expect(2).To(Equal(len(query))) 113 | Expect(query["hello"]).ToNot(BeNil()) 114 | Expect(query["world"]).ToNot(BeNil()) 115 | Expect(1).To(Equal(len(query["hello"]))) 116 | Expect(1).To(Equal(len(query["world"]))) 117 | Expect("1").To(Equal(query["hello"][0])) 118 | Expect("2").To(Equal(query["world"][0])) 119 | }) 120 | 121 | mvhRequest := getProxyRequest("/hello", "GET") 122 | mvhRequest.MultiValueHeaders = map[string][]string{ 123 | "hello": {"1"}, 124 | "world": {"2", "3"}, 125 | } 126 | It("Populates multiple value headers correctly", func() { 127 | httpReq, err := accessor.EventToRequestWithContext(context.Background(), mvhRequest) 128 | Expect(err).To(BeNil()) 129 | Expect("/hello").To(Equal(httpReq.URL.Path)) 130 | Expect("GET").To(Equal(httpReq.Method)) 131 | 132 | headers := httpReq.Header 133 | Expect(2).To(Equal(len(headers))) 134 | 135 | for k, value := range headers { 136 | Expect(value).To(Equal(mvhRequest.MultiValueHeaders[strings.ToLower(k)])) 137 | } 138 | }) 139 | 140 | svhRequest := getProxyRequest("/hello", "GET") 141 | svhRequest.Headers = map[string]string{ 142 | "hello": "1", 143 | "world": "2", 144 | } 145 | It("Populates single value headers correctly", func() { 146 | httpReq, err := accessor.EventToRequestWithContext(context.Background(), svhRequest) 147 | Expect(err).To(BeNil()) 148 | Expect("/hello").To(Equal(httpReq.URL.Path)) 149 | Expect("GET").To(Equal(httpReq.Method)) 150 | 151 | headers := httpReq.Header 152 | Expect(2).To(Equal(len(headers))) 153 | 154 | for k, value := range headers { 155 | Expect(value[0]).To(Equal(svhRequest.Headers[strings.ToLower(k)])) 156 | } 157 | }) 158 | 159 | basePathRequest := getProxyRequest("/app1/orders", "GET") 160 | 161 | It("Stips the base path correct", func() { 162 | accessor.StripBasePath("app1") 163 | httpReq, err := accessor.EventToRequestWithContext(context.Background(), basePathRequest) 164 | 165 | Expect(err).To(BeNil()) 166 | Expect("/orders").To(Equal(httpReq.URL.Path)) 167 | Expect("/orders").To(Equal(httpReq.RequestURI)) 168 | }) 169 | 170 | contextRequest := getProxyRequest("orders", "GET") 171 | contextRequest.RequestContext = getRequestContext() 172 | 173 | It("Populates context header correctly", func() { 174 | // calling old method to verify reverse compatibility 175 | httpReq, err := accessor.ProxyEventToHTTPRequest(contextRequest) 176 | Expect(err).To(BeNil()) 177 | Expect(2).To(Equal(len(httpReq.Header))) 178 | Expect(httpReq.Header.Get(core.APIGwContextHeader)).ToNot(BeNil()) 179 | }) 180 | }) 181 | 182 | Context("StripBasePath tests", func() { 183 | accessor := core.RequestAccessor{} 184 | It("Adds prefix slash", func() { 185 | basePath := accessor.StripBasePath("app1") 186 | Expect("/app1").To(Equal(basePath)) 187 | }) 188 | 189 | It("Removes trailing slash", func() { 190 | basePath := accessor.StripBasePath("/app1/") 191 | Expect("/app1").To(Equal(basePath)) 192 | }) 193 | 194 | It("Ignores blank strings", func() { 195 | basePath := accessor.StripBasePath(" ") 196 | Expect("").To(Equal(basePath)) 197 | }) 198 | }) 199 | 200 | Context("Retrieves API Gateway context", func() { 201 | It("Returns a correctly unmarshalled object", func() { 202 | contextRequest := getProxyRequest("orders", "GET") 203 | contextRequest.RequestContext = getRequestContext() 204 | 205 | accessor := core.RequestAccessor{} 206 | // calling old method to verify reverse compatibility 207 | httpReq, err := accessor.ProxyEventToHTTPRequest(contextRequest) 208 | Expect(err).To(BeNil()) 209 | 210 | headerContext, err := accessor.GetAPIGatewayContext(httpReq) 211 | Expect(err).To(BeNil()) 212 | Expect(headerContext).ToNot(BeNil()) 213 | Expect("x").To(Equal(headerContext.AccountID)) 214 | Expect("x").To(Equal(headerContext.RequestID)) 215 | Expect("x").To(Equal(headerContext.APIID)) 216 | proxyContext, ok := core.GetAPIGatewayContextFromContext(httpReq.Context()) 217 | // should fail because using header proxy method 218 | Expect(ok).To(BeFalse()) 219 | 220 | // overwrite existing context header 221 | contextRequestWithHeaders := getProxyRequest("orders", "GET") 222 | contextRequestWithHeaders.RequestContext = getRequestContext() 223 | contextRequestWithHeaders.Headers = map[string]string{core.APIGwContextHeader: `{"AccountID":"abc123"}`} 224 | httpReq, err = accessor.ProxyEventToHTTPRequest(contextRequestWithHeaders) 225 | Expect(err).To(BeNil()) 226 | headerContext, err = accessor.GetAPIGatewayContext(httpReq) 227 | Expect(err).To(BeNil()) 228 | Expect(headerContext.AccountID).To(Equal("x")) 229 | 230 | httpReq, err = accessor.EventToRequestWithContext(context.Background(), contextRequest) 231 | Expect(err).To(BeNil()) 232 | proxyContext, ok = core.GetAPIGatewayContextFromContext(httpReq.Context()) 233 | Expect(ok).To(BeTrue()) 234 | Expect("x").To(Equal(proxyContext.APIID)) 235 | Expect("x").To(Equal(proxyContext.RequestID)) 236 | Expect("x").To(Equal(proxyContext.APIID)) 237 | Expect("prod").To(Equal(proxyContext.Stage)) 238 | runtimeContext, ok := core.GetRuntimeContextFromContext(httpReq.Context()) 239 | Expect(ok).To(BeTrue()) 240 | Expect(runtimeContext).To(BeNil()) 241 | 242 | lambdaContext := lambdacontext.NewContext(context.Background(), &lambdacontext.LambdaContext{AwsRequestID: "abc123"}) 243 | httpReq, err = accessor.EventToRequestWithContext(lambdaContext, contextRequest) 244 | Expect(err).To(BeNil()) 245 | 246 | headerContext, err = accessor.GetAPIGatewayContext(httpReq) 247 | // should fail as new context method doesn't populate headers 248 | Expect(err).ToNot(BeNil()) 249 | proxyContext, ok = core.GetAPIGatewayContextFromContext(httpReq.Context()) 250 | Expect(ok).To(BeTrue()) 251 | Expect("x").To(Equal(proxyContext.APIID)) 252 | Expect("x").To(Equal(proxyContext.RequestID)) 253 | Expect("x").To(Equal(proxyContext.APIID)) 254 | Expect("prod").To(Equal(proxyContext.Stage)) 255 | runtimeContext, ok = core.GetRuntimeContextFromContext(httpReq.Context()) 256 | Expect(ok).To(BeTrue()) 257 | Expect(runtimeContext).ToNot(BeNil()) 258 | Expect("abc123").To(Equal(runtimeContext.AwsRequestID)) 259 | }) 260 | 261 | It("Populates stage variables correctly", func() { 262 | varsRequest := getProxyRequest("orders", "GET") 263 | varsRequest.StageVariables = getStageVariables() 264 | 265 | accessor := core.RequestAccessor{} 266 | httpReq, err := accessor.ProxyEventToHTTPRequest(varsRequest) 267 | Expect(err).To(BeNil()) 268 | 269 | stageVars, err := accessor.GetAPIGatewayStageVars(httpReq) 270 | Expect(err).To(BeNil()) 271 | Expect(2).To(Equal(len(stageVars))) 272 | Expect(stageVars["var1"]).ToNot(BeNil()) 273 | Expect(stageVars["var2"]).ToNot(BeNil()) 274 | Expect("value1").To(Equal(stageVars["var1"])) 275 | Expect("value2").To(Equal(stageVars["var2"])) 276 | 277 | // overwrite existing stagevars header 278 | varsRequestWithHeaders := getProxyRequest("orders", "GET") 279 | varsRequestWithHeaders.StageVariables = getStageVariables() 280 | varsRequestWithHeaders.Headers = map[string]string{core.APIGwStageVarsHeader: `{"var1":"abc123"}`} 281 | httpReq, err = accessor.ProxyEventToHTTPRequest(varsRequestWithHeaders) 282 | Expect(err).To(BeNil()) 283 | stageVars, err = accessor.GetAPIGatewayStageVars(httpReq) 284 | Expect(err).To(BeNil()) 285 | Expect(stageVars["var1"]).To(Equal("value1")) 286 | 287 | stageVars, ok := core.GetStageVarsFromContext(httpReq.Context()) 288 | // not present in context 289 | Expect(ok).To(BeFalse()) 290 | 291 | httpReq, err = accessor.EventToRequestWithContext(context.Background(), varsRequest) 292 | Expect(err).To(BeNil()) 293 | 294 | stageVars, err = accessor.GetAPIGatewayStageVars(httpReq) 295 | // should not be in headers 296 | Expect(err).ToNot(BeNil()) 297 | 298 | stageVars, ok = core.GetStageVarsFromContext(httpReq.Context()) 299 | Expect(ok).To(BeTrue()) 300 | Expect(2).To(Equal(len(stageVars))) 301 | Expect(stageVars["var1"]).ToNot(BeNil()) 302 | Expect(stageVars["var2"]).ToNot(BeNil()) 303 | Expect("value1").To(Equal(stageVars["var1"])) 304 | Expect("value2").To(Equal(stageVars["var2"])) 305 | }) 306 | 307 | It("Populates the default hostname correctly", func() { 308 | 309 | basicRequest := getProxyRequest("orders", "GET") 310 | basicRequest.RequestContext = getRequestContext() 311 | accessor := core.RequestAccessor{} 312 | httpReq, err := accessor.ProxyEventToHTTPRequest(basicRequest) 313 | Expect(err).To(BeNil()) 314 | 315 | Expect(basicRequest.RequestContext.DomainName).To(Equal(httpReq.Host)) 316 | Expect(basicRequest.RequestContext.DomainName).To(Equal(httpReq.URL.Host)) 317 | }) 318 | 319 | It("Uses a custom hostname", func() { 320 | myCustomHost := "http://my-custom-host.com" 321 | os.Setenv(core.CustomHostVariable, myCustomHost) 322 | basicRequest := getProxyRequest("orders", "GET") 323 | accessor := core.RequestAccessor{} 324 | httpReq, err := accessor.EventToRequestWithContext(context.Background(), basicRequest) 325 | Expect(err).To(BeNil()) 326 | 327 | Expect(myCustomHost).To(Equal("http://" + httpReq.Host)) 328 | Expect(myCustomHost).To(Equal("http://" + httpReq.URL.Host)) 329 | os.Unsetenv(core.CustomHostVariable) 330 | }) 331 | 332 | It("Strips terminating / from hostname", func() { 333 | myCustomHost := "http://my-custom-host.com" 334 | os.Setenv(core.CustomHostVariable, myCustomHost+"/") 335 | basicRequest := getProxyRequest("orders", "GET") 336 | accessor := core.RequestAccessor{} 337 | httpReq, err := accessor.EventToRequestWithContext(context.Background(), basicRequest) 338 | Expect(err).To(BeNil()) 339 | 340 | Expect(myCustomHost).To(Equal("http://" + httpReq.Host)) 341 | Expect(myCustomHost).To(Equal("http://" + httpReq.URL.Host)) 342 | os.Unsetenv(core.CustomHostVariable) 343 | }) 344 | }) 345 | }) 346 | 347 | func getProxyRequest(path string, method string) events.APIGatewayProxyRequest { 348 | return events.APIGatewayProxyRequest{ 349 | Path: path, 350 | HTTPMethod: method, 351 | } 352 | } 353 | 354 | func getRequestContext() events.APIGatewayProxyRequestContext { 355 | return events.APIGatewayProxyRequestContext{ 356 | AccountID: "x", 357 | RequestID: "x", 358 | APIID: "x", 359 | Stage: "prod", 360 | DomainName: "12abcdefgh.execute-api.us-east-2.amazonaws.com", 361 | } 362 | } 363 | 364 | func getStageVariables() map[string]string { 365 | return map[string]string{ 366 | "var1": "value1", 367 | "var2": "value2", 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /core/requestv2.go: -------------------------------------------------------------------------------- 1 | // Package core provides utility methods that help convert proxy events 2 | // into an http.Request and http.ResponseWriter 3 | package core 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "encoding/base64" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "log" 13 | "net/http" 14 | "net/textproto" 15 | "net/url" 16 | "os" 17 | "strings" 18 | 19 | "github.com/aws/aws-lambda-go/events" 20 | "github.com/aws/aws-lambda-go/lambdacontext" 21 | ) 22 | 23 | // RequestAccessorV2 objects give access to custom API Gateway properties 24 | // in the request. 25 | type RequestAccessorV2 struct { 26 | stripBasePath string 27 | } 28 | 29 | // GetAPIGatewayContextV2 extracts the API Gateway context object from a 30 | // request's custom header. 31 | // Returns a populated events.APIGatewayProxyRequestContext object from 32 | // the request. 33 | func (r *RequestAccessorV2) GetAPIGatewayContextV2(req *http.Request) (events.APIGatewayV2HTTPRequestContext, error) { 34 | if req.Header.Get(APIGwContextHeader) == "" { 35 | return events.APIGatewayV2HTTPRequestContext{}, errors.New("No context header in request") 36 | } 37 | context := events.APIGatewayV2HTTPRequestContext{} 38 | err := json.Unmarshal([]byte(req.Header.Get(APIGwContextHeader)), &context) 39 | if err != nil { 40 | log.Println("Erorr while unmarshalling context") 41 | log.Println(err) 42 | return events.APIGatewayV2HTTPRequestContext{}, err 43 | } 44 | return context, nil 45 | } 46 | 47 | // GetAPIGatewayStageVars extracts the API Gateway stage variables from a 48 | // request's custom header. 49 | // Returns a map[string]string of the stage variables and their values from 50 | // the request. 51 | func (r *RequestAccessorV2) GetAPIGatewayStageVars(req *http.Request) (map[string]string, error) { 52 | stageVars := make(map[string]string) 53 | if req.Header.Get(APIGwStageVarsHeader) == "" { 54 | return stageVars, errors.New("No stage vars header in request") 55 | } 56 | err := json.Unmarshal([]byte(req.Header.Get(APIGwStageVarsHeader)), &stageVars) 57 | if err != nil { 58 | log.Println("Erorr while unmarshalling stage variables") 59 | log.Println(err) 60 | return stageVars, err 61 | } 62 | return stageVars, nil 63 | } 64 | 65 | // StripBasePath instructs the RequestAccessor object that the given base 66 | // path should be removed from the request path before sending it to the 67 | // framework for routing. This is used when API Gateway is configured with 68 | // base path mappings in custom domain names. 69 | func (r *RequestAccessorV2) StripBasePath(basePath string) string { 70 | if strings.Trim(basePath, " ") == "" { 71 | r.stripBasePath = "" 72 | return "" 73 | } 74 | 75 | newBasePath := basePath 76 | if !strings.HasPrefix(newBasePath, "/") { 77 | newBasePath = "/" + newBasePath 78 | } 79 | 80 | if strings.HasSuffix(newBasePath, "/") { 81 | newBasePath = newBasePath[:len(newBasePath)-1] 82 | } 83 | 84 | r.stripBasePath = newBasePath 85 | 86 | return newBasePath 87 | } 88 | 89 | // ProxyEventToHTTPRequest converts an API Gateway proxy event into a http.Request object. 90 | // Returns the populated http request with additional two custom headers for the stage variables and API Gateway context. 91 | // To access these properties use the GetAPIGatewayStageVars and GetAPIGatewayContext method of the RequestAccessor object. 92 | func (r *RequestAccessorV2) ProxyEventToHTTPRequest(req events.APIGatewayV2HTTPRequest) (*http.Request, error) { 93 | httpRequest, err := r.EventToRequest(req) 94 | if err != nil { 95 | log.Println(err) 96 | return nil, err 97 | } 98 | return addToHeaderV2(httpRequest, req) 99 | } 100 | 101 | // EventToRequestWithContext converts an API Gateway proxy event and context into an http.Request object. 102 | // Returns the populated http request with lambda context, stage variables and APIGatewayProxyRequestContext as part of its context. 103 | // Access those using GetAPIGatewayContextFromContext, GetStageVarsFromContext and GetRuntimeContextFromContext functions in this package. 104 | func (r *RequestAccessorV2) EventToRequestWithContext(ctx context.Context, req events.APIGatewayV2HTTPRequest) (*http.Request, error) { 105 | httpRequest, err := r.EventToRequest(req) 106 | if err != nil { 107 | log.Println(err) 108 | return nil, err 109 | } 110 | return addToContextV2(ctx, httpRequest, req), nil 111 | } 112 | 113 | // EventToRequest converts an API Gateway proxy event into an http.Request object. 114 | // Returns the populated request maintaining headers 115 | func (r *RequestAccessorV2) EventToRequest(req events.APIGatewayV2HTTPRequest) (*http.Request, error) { 116 | decodedBody := []byte(req.Body) 117 | if req.IsBase64Encoded { 118 | base64Body, err := base64.StdEncoding.DecodeString(req.Body) 119 | if err != nil { 120 | return nil, err 121 | } 122 | decodedBody = base64Body 123 | } 124 | 125 | path := req.RawPath 126 | 127 | // if RawPath empty is, populate from request context 128 | if len(path) == 0 { 129 | path = req.RequestContext.HTTP.Path 130 | } 131 | 132 | if r.stripBasePath != "" && len(r.stripBasePath) > 1 { 133 | if strings.HasPrefix(path, r.stripBasePath) { 134 | path = strings.Replace(path, r.stripBasePath, "", 1) 135 | } 136 | } 137 | if !strings.HasPrefix(path, "/") { 138 | path = "/" + path 139 | } 140 | serverAddress := "https://" + req.RequestContext.DomainName 141 | if customAddress, ok := os.LookupEnv(CustomHostVariable); ok { 142 | serverAddress = customAddress 143 | } 144 | path = serverAddress + path 145 | 146 | if len(req.RawQueryString) > 0 { 147 | path += "?" + req.RawQueryString 148 | } else if len(req.QueryStringParameters) > 0 { 149 | values := url.Values{} 150 | for key, value := range req.QueryStringParameters { 151 | values.Add(key, value) 152 | } 153 | path += "?" + values.Encode() 154 | } 155 | 156 | httpRequest, err := http.NewRequest( 157 | strings.ToUpper(req.RequestContext.HTTP.Method), 158 | path, 159 | bytes.NewReader(decodedBody), 160 | ) 161 | 162 | if err != nil { 163 | fmt.Printf("Could not convert request %s:%s to http.Request\n", req.RequestContext.HTTP.Method, req.RequestContext.HTTP.Path) 164 | log.Println(err) 165 | return nil, err 166 | } 167 | 168 | httpRequest.RemoteAddr = req.RequestContext.HTTP.SourceIP 169 | 170 | for _, cookie := range req.Cookies { 171 | httpRequest.Header.Add("Cookie", cookie) 172 | } 173 | 174 | singletonHeaders, headers := splitSingletonHeaders(req.Headers) 175 | 176 | for headerKey, headerValue := range singletonHeaders { 177 | httpRequest.Header.Add(headerKey, headerValue) 178 | } 179 | 180 | for headerKey, headerValue := range headers { 181 | for _, val := range strings.Split(headerValue, ",") { 182 | httpRequest.Header.Add(headerKey, strings.Trim(val, " ")) 183 | } 184 | } 185 | 186 | httpRequest.RequestURI = httpRequest.URL.RequestURI() 187 | 188 | return httpRequest, nil 189 | } 190 | 191 | func addToHeaderV2(req *http.Request, apiGwRequest events.APIGatewayV2HTTPRequest) (*http.Request, error) { 192 | stageVars, err := json.Marshal(apiGwRequest.StageVariables) 193 | if err != nil { 194 | log.Println("Could not marshal stage variables for custom header") 195 | return nil, err 196 | } 197 | req.Header.Add(APIGwStageVarsHeader, string(stageVars)) 198 | apiGwContext, err := json.Marshal(apiGwRequest.RequestContext) 199 | if err != nil { 200 | log.Println("Could not Marshal API GW context for custom header") 201 | return req, err 202 | } 203 | req.Header.Add(APIGwContextHeader, string(apiGwContext)) 204 | return req, nil 205 | } 206 | 207 | func addToContextV2(ctx context.Context, req *http.Request, apiGwRequest events.APIGatewayV2HTTPRequest) *http.Request { 208 | lc, _ := lambdacontext.FromContext(ctx) 209 | rc := requestContextV2{lambdaContext: lc, gatewayProxyContext: apiGwRequest.RequestContext, stageVars: apiGwRequest.StageVariables} 210 | ctx = context.WithValue(ctx, ctxKey{}, rc) 211 | return req.WithContext(ctx) 212 | } 213 | 214 | // GetAPIGatewayV2ContextFromContext retrieve APIGatewayProxyRequestContext from context.Context 215 | func GetAPIGatewayV2ContextFromContext(ctx context.Context) (events.APIGatewayV2HTTPRequestContext, bool) { 216 | v, ok := ctx.Value(ctxKey{}).(requestContextV2) 217 | return v.gatewayProxyContext, ok 218 | } 219 | 220 | // GetRuntimeContextFromContextV2 retrieve Lambda Runtime Context from context.Context 221 | func GetRuntimeContextFromContextV2(ctx context.Context) (*lambdacontext.LambdaContext, bool) { 222 | v, ok := ctx.Value(ctxKey{}).(requestContextV2) 223 | return v.lambdaContext, ok 224 | } 225 | 226 | // GetStageVarsFromContextV2 retrieve stage variables from context 227 | func GetStageVarsFromContextV2(ctx context.Context) (map[string]string, bool) { 228 | v, ok := ctx.Value(ctxKey{}).(requestContextV2) 229 | return v.stageVars, ok 230 | } 231 | 232 | type requestContextV2 struct { 233 | lambdaContext *lambdacontext.LambdaContext 234 | gatewayProxyContext events.APIGatewayV2HTTPRequestContext 235 | stageVars map[string]string 236 | } 237 | 238 | // splitSingletonHeaders splits the headers into single-value headers and other, 239 | // multi-value capable, headers. 240 | // Returns (single-value headers, multi-value-capable headers) 241 | func splitSingletonHeaders(headers map[string]string) (map[string]string, map[string]string) { 242 | singletons := make(map[string]string) 243 | multitons := make(map[string]string) 244 | for headerKey, headerValue := range headers { 245 | if ok := singletonHeaders[textproto.CanonicalMIMEHeaderKey(headerKey)]; ok { 246 | singletons[headerKey] = headerValue 247 | } else { 248 | multitons[headerKey] = headerValue 249 | } 250 | } 251 | 252 | return singletons, multitons 253 | } 254 | 255 | // singletonHeaders is a set of headers, that only accept a single 256 | // value which may be comma separated (according to RFC 7230) 257 | var singletonHeaders = map[string]bool{ 258 | "Content-Type": true, 259 | "Content-Disposition": true, 260 | "Content-Length": true, 261 | "User-Agent": true, 262 | "Referer": true, 263 | "Host": true, 264 | "Authorization": true, 265 | "Proxy-Authorization": true, 266 | "If-Modified-Since": true, 267 | "If-Unmodified-Since": true, 268 | "From": true, 269 | "Location": true, 270 | "Max-Forwards": true, 271 | } 272 | -------------------------------------------------------------------------------- /core/response.go: -------------------------------------------------------------------------------- 1 | // Package core provides utility methods that help convert proxy events 2 | // into an http.Request and http.ResponseWriter 3 | package core 4 | 5 | import ( 6 | "bytes" 7 | "encoding/base64" 8 | "errors" 9 | "net/http" 10 | "unicode/utf8" 11 | 12 | "github.com/aws/aws-lambda-go/events" 13 | ) 14 | 15 | const ( 16 | defaultStatusCode = -1 17 | contentTypeHeaderKey = "Content-Type" 18 | ) 19 | 20 | // ProxyResponseWriter implements http.ResponseWriter and adds the method 21 | // necessary to return an events.APIGatewayProxyResponse object 22 | type ProxyResponseWriter struct { 23 | headers http.Header 24 | body bytes.Buffer 25 | status int 26 | observers []chan<- bool 27 | } 28 | 29 | // NewProxyResponseWriter returns a new ProxyResponseWriter object. 30 | // The object is initialized with an empty map of headers and a 31 | // status code of -1 32 | func NewProxyResponseWriter() *ProxyResponseWriter { 33 | return &ProxyResponseWriter{ 34 | headers: make(http.Header), 35 | status: defaultStatusCode, 36 | observers: make([]chan<- bool, 0), 37 | } 38 | 39 | } 40 | 41 | func (r *ProxyResponseWriter) CloseNotify() <-chan bool { 42 | ch := make(chan bool, 1) 43 | 44 | r.observers = append(r.observers, ch) 45 | 46 | return ch 47 | } 48 | 49 | func (r *ProxyResponseWriter) notifyClosed() { 50 | for _, v := range r.observers { 51 | v <- true 52 | } 53 | } 54 | 55 | // Header implementation from the http.ResponseWriter interface. 56 | func (r *ProxyResponseWriter) Header() http.Header { 57 | return r.headers 58 | } 59 | 60 | // Write sets the response body in the object. If no status code 61 | // was set before with the WriteHeader method it sets the status 62 | // for the response to 200 OK. 63 | func (r *ProxyResponseWriter) Write(body []byte) (int, error) { 64 | if r.status == defaultStatusCode { 65 | r.status = http.StatusOK 66 | } 67 | 68 | // if the content type header is not set when we write the body we try to 69 | // detect one and set it by default. If the content type cannot be detected 70 | // it is automatically set to "application/octet-stream" by the 71 | // DetectContentType method 72 | if r.Header().Get(contentTypeHeaderKey) == "" { 73 | r.Header().Add(contentTypeHeaderKey, http.DetectContentType(body)) 74 | } 75 | 76 | return (&r.body).Write(body) 77 | } 78 | 79 | // WriteHeader sets a status code for the response. This method is used 80 | // for error responses. 81 | func (r *ProxyResponseWriter) WriteHeader(status int) { 82 | r.status = status 83 | } 84 | 85 | 86 | // Flush implements the Flusher interface which is called by 87 | // some implementers. This is intentionally a no-op 88 | func (r *ProxyResponseWriter) Flush() { 89 | //no-op 90 | } 91 | 92 | // GetProxyResponse converts the data passed to the response writer into 93 | // an events.APIGatewayProxyResponse object. 94 | // Returns a populated proxy response object. If the response is invalid, for example 95 | // has no headers or an invalid status code returns an error. 96 | func (r *ProxyResponseWriter) GetProxyResponse() (events.APIGatewayProxyResponse, error) { 97 | r.notifyClosed() 98 | 99 | if r.status == defaultStatusCode { 100 | return events.APIGatewayProxyResponse{}, errors.New("Status code not set on response") 101 | } 102 | 103 | var output string 104 | isBase64 := false 105 | 106 | bb := (&r.body).Bytes() 107 | 108 | if utf8.Valid(bb) { 109 | output = string(bb) 110 | } else { 111 | output = base64.StdEncoding.EncodeToString(bb) 112 | isBase64 = true 113 | } 114 | 115 | return events.APIGatewayProxyResponse{ 116 | StatusCode: r.status, 117 | MultiValueHeaders: http.Header(r.headers), 118 | Body: output, 119 | IsBase64Encoded: isBase64, 120 | }, nil 121 | } 122 | -------------------------------------------------------------------------------- /core/responseALB.go: -------------------------------------------------------------------------------- 1 | // Package core provides utility methods that help convert proxy events 2 | // into an http.Request and http.ResponseWriter 3 | package core 4 | 5 | import ( 6 | "bytes" 7 | "encoding/base64" 8 | "errors" 9 | "net/http" 10 | "unicode/utf8" 11 | 12 | "github.com/aws/aws-lambda-go/events" 13 | ) 14 | 15 | // ProxyResponseWriter implements http.ResponseWriter and adds the method 16 | // necessary to return an events.ALBTargetGroupResponse object 17 | type ProxyResponseWriterALB struct { 18 | headers http.Header 19 | body bytes.Buffer 20 | status int 21 | statusText string 22 | observers []chan<- bool 23 | } 24 | 25 | // NewProxyResponseWriter returns a new ProxyResponseWriter object. 26 | // The object is initialized with an empty map of headers and a 27 | // status code of -1 28 | func NewProxyResponseWriterALB() *ProxyResponseWriterALB { 29 | return &ProxyResponseWriterALB{ 30 | headers: make(http.Header), 31 | status: defaultStatusCode, 32 | statusText: http.StatusText(defaultStatusCode), 33 | observers: make([]chan<- bool, 0), 34 | } 35 | 36 | } 37 | 38 | func (r *ProxyResponseWriterALB) CloseNotify() <-chan bool { 39 | ch := make(chan bool, 1) 40 | 41 | r.observers = append(r.observers, ch) 42 | 43 | return ch 44 | } 45 | 46 | func (r *ProxyResponseWriterALB) notifyClosed() { 47 | for _, v := range r.observers { 48 | v <- true 49 | } 50 | } 51 | 52 | // Header implementation from the http.ResponseWriter interface. 53 | func (r *ProxyResponseWriterALB) Header() http.Header { 54 | return r.headers 55 | } 56 | 57 | // Write sets the response body in the object. If no status code 58 | // was set before with the WriteHeader method it sets the status 59 | // for the response to 200 OK. 60 | func (r *ProxyResponseWriterALB) Write(body []byte) (int, error) { 61 | if r.status == defaultStatusCode { 62 | r.status = http.StatusOK 63 | } 64 | 65 | // if the content type header is not set when we write the body we try to 66 | // detect one and set it by default. If the content type cannot be detected 67 | // it is automatically set to "application/octet-stream" by the 68 | // DetectContentType method 69 | if r.Header().Get(contentTypeHeaderKey) == "" { 70 | r.Header().Add(contentTypeHeaderKey, http.DetectContentType(body)) 71 | } 72 | 73 | return (&r.body).Write(body) 74 | } 75 | 76 | // WriteHeader sets a status code for the response. This method is used 77 | // for error responses. 78 | func (r *ProxyResponseWriterALB) WriteHeader(status int) { 79 | r.status = status 80 | } 81 | 82 | // GetProxyResponse converts the data passed to the response writer into 83 | // an events.ALBTargetGroupResponse object. 84 | // Returns a populated proxy response object. If the response is invalid, for example 85 | // has no headers or an invalid status code returns an error. 86 | func (r *ProxyResponseWriterALB) GetProxyResponse() (events.ALBTargetGroupResponse, error) { 87 | r.notifyClosed() 88 | 89 | if r.status == defaultStatusCode { 90 | return events.ALBTargetGroupResponse{}, errors.New("status code not set on response") 91 | } 92 | 93 | var output string 94 | isBase64 := false 95 | 96 | bb := (&r.body).Bytes() 97 | 98 | if utf8.Valid(bb) { 99 | output = string(bb) 100 | } else { 101 | output = base64.StdEncoding.EncodeToString(bb) 102 | isBase64 = true 103 | } 104 | 105 | return events.ALBTargetGroupResponse{ 106 | StatusCode: r.status, 107 | StatusDescription: http.StatusText(r.status), 108 | MultiValueHeaders: http.Header(r.headers), 109 | Body: output, 110 | IsBase64Encoded: isBase64, 111 | }, nil 112 | } 113 | -------------------------------------------------------------------------------- /core/responseALB_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/base64" 5 | "math/rand" 6 | "net/http" 7 | "strings" 8 | 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("ResponseWriterALB tests", func() { 14 | Context("ALB writing to response object", func() { 15 | response := NewProxyResponseWriterALB() 16 | 17 | It("Sets the correct default status", func() { 18 | Expect(defaultStatusCode).To(Equal(response.status)) 19 | }) 20 | 21 | It("Initializes the headers map", func() { 22 | Expect(response.headers).ToNot(BeNil()) 23 | Expect(0).To(Equal(len(response.headers))) 24 | }) 25 | 26 | It("Writes headers correctly", func() { 27 | response.Header().Add("Content-Type", "application/json") 28 | 29 | Expect(1).To(Equal(len(response.headers))) 30 | Expect("application/json").To(Equal(response.headers["Content-Type"][0])) 31 | }) 32 | 33 | It("Writes body content correctly", func() { 34 | binaryBody := make([]byte, 256) 35 | _, err := rand.Read(binaryBody) 36 | Expect(err).To(BeNil()) 37 | 38 | written, err := response.Write(binaryBody) 39 | Expect(err).To(BeNil()) 40 | Expect(len(binaryBody)).To(Equal(written)) 41 | }) 42 | 43 | It("Automatically set the status code to 200", func() { 44 | Expect(http.StatusOK).To(Equal(response.status)) 45 | }) 46 | 47 | It("Forces the status to a new code", func() { 48 | response.WriteHeader(http.StatusAccepted) 49 | Expect(http.StatusAccepted).To(Equal(response.status)) 50 | }) 51 | }) 52 | 53 | Context("Automatically set response content type", func() { 54 | xmlBodyContent := "ToveJaniReminderDon't forget me this weekend!" 55 | htmlBodyContent := " Title of the documentContent of the document......" 56 | It("Does not set the content type if it's already set", func() { 57 | resp := NewProxyResponseWriterALB() 58 | resp.Header().Add("Content-Type", "application/json") 59 | 60 | resp.Write([]byte(xmlBodyContent)) 61 | 62 | Expect("application/json").To(Equal(resp.Header().Get("Content-Type"))) 63 | proxyResp, err := resp.GetProxyResponse() 64 | Expect(err).To(BeNil()) 65 | Expect(1).To(Equal(len(proxyResp.MultiValueHeaders))) 66 | Expect("application/json").To(Equal(proxyResp.MultiValueHeaders["Content-Type"][0])) 67 | Expect(xmlBodyContent).To(Equal(proxyResp.Body)) 68 | }) 69 | 70 | It("Sets the content type to text/xml given the body", func() { 71 | resp := NewProxyResponseWriterALB() 72 | resp.Write([]byte(xmlBodyContent)) 73 | 74 | Expect("").ToNot(Equal(resp.Header().Get("Content-Type"))) 75 | Expect(true).To(Equal(strings.HasPrefix(resp.Header().Get("Content-Type"), "text/xml;"))) 76 | proxyResp, err := resp.GetProxyResponse() 77 | Expect(err).To(BeNil()) 78 | Expect(1).To(Equal(len(proxyResp.MultiValueHeaders))) 79 | Expect(true).To(Equal(strings.HasPrefix(proxyResp.MultiValueHeaders["Content-Type"][0], "text/xml;"))) 80 | Expect(xmlBodyContent).To(Equal(proxyResp.Body)) 81 | }) 82 | 83 | It("Sets the content type to text/html given the body", func() { 84 | resp := NewProxyResponseWriterALB() 85 | resp.Write([]byte(htmlBodyContent)) 86 | 87 | Expect("").ToNot(Equal(resp.Header().Get("Content-Type"))) 88 | Expect(true).To(Equal(strings.HasPrefix(resp.Header().Get("Content-Type"), "text/html;"))) 89 | proxyResp, err := resp.GetProxyResponse() 90 | Expect(err).To(BeNil()) 91 | Expect(1).To(Equal(len(proxyResp.MultiValueHeaders))) 92 | Expect(true).To(Equal(strings.HasPrefix(proxyResp.MultiValueHeaders["Content-Type"][0], "text/html;"))) 93 | Expect(htmlBodyContent).To(Equal(proxyResp.Body)) 94 | }) 95 | }) 96 | 97 | Context("Export ALB Target Group Response", func() { 98 | emtpyResponse := NewProxyResponseWriterALB() 99 | emtpyResponse.Header().Add("Content-Type", "application/json") 100 | 101 | It("Refuses empty responses with default status code", func() { 102 | _, err := emtpyResponse.GetProxyResponse() 103 | Expect(err).ToNot(BeNil()) 104 | Expect("status code not set on response").To(Equal(err.Error())) 105 | }) 106 | 107 | simpleResponse := NewProxyResponseWriterALB() 108 | simpleResponse.Write([]byte("hello")) 109 | simpleResponse.Header().Add("Content-Type", "text/plain") 110 | It("Writes text body correctly", func() { 111 | proxyResponse, err := simpleResponse.GetProxyResponse() 112 | Expect(err).To(BeNil()) 113 | Expect(proxyResponse).ToNot(BeNil()) 114 | 115 | Expect("hello").To(Equal(proxyResponse.Body)) 116 | Expect(http.StatusOK).To(Equal(proxyResponse.StatusCode)) 117 | Expect(1).To(Equal(len(proxyResponse.MultiValueHeaders))) 118 | Expect(true).To(Equal(strings.HasPrefix(proxyResponse.MultiValueHeaders["Content-Type"][0], "text/plain"))) 119 | Expect(proxyResponse.IsBase64Encoded).To(BeFalse()) 120 | }) 121 | 122 | binaryResponse := NewProxyResponseWriterALB() 123 | binaryResponse.Header().Add("Content-Type", "application/octet-stream") 124 | binaryBody := make([]byte, 256) 125 | _, err := rand.Read(binaryBody) 126 | if err != nil { 127 | Fail("Could not generate random binary body") 128 | } 129 | binaryResponse.Write(binaryBody) 130 | binaryResponse.WriteHeader(http.StatusAccepted) 131 | 132 | It("Encodes binary responses correctly", func() { 133 | proxyResponse, err := binaryResponse.GetProxyResponse() 134 | Expect(err).To(BeNil()) 135 | Expect(proxyResponse).ToNot(BeNil()) 136 | 137 | Expect(proxyResponse.IsBase64Encoded).To(BeTrue()) 138 | Expect(base64.StdEncoding.EncodedLen(len(binaryBody))).To(Equal(len(proxyResponse.Body))) 139 | 140 | Expect(base64.StdEncoding.EncodeToString(binaryBody)).To(Equal(proxyResponse.Body)) 141 | Expect(1).To(Equal(len(proxyResponse.MultiValueHeaders))) 142 | Expect("application/octet-stream").To(Equal(proxyResponse.MultiValueHeaders["Content-Type"][0])) 143 | Expect(http.StatusAccepted).To(Equal(proxyResponse.StatusCode)) 144 | }) 145 | }) 146 | 147 | Context("Handle multi-value headers", func() { 148 | 149 | It("Writes single-value headers correctly", func() { 150 | response := NewProxyResponseWriterALB() 151 | response.Header().Add("Content-Type", "application/json") 152 | response.Write([]byte("hello")) 153 | proxyResponse, err := response.GetProxyResponse() 154 | Expect(err).To(BeNil()) 155 | 156 | // Headers are not also written to `Headers` field 157 | Expect(0).To(Equal(len(proxyResponse.Headers))) 158 | Expect(1).To(Equal(len(proxyResponse.MultiValueHeaders["Content-Type"]))) 159 | Expect("application/json").To(Equal(proxyResponse.MultiValueHeaders["Content-Type"][0])) 160 | }) 161 | 162 | It("Writes multi-value headers correctly", func() { 163 | response := NewProxyResponseWriterALB() 164 | response.Header().Add("Set-Cookie", "csrftoken=foobar") 165 | response.Header().Add("Set-Cookie", "session_id=barfoo") 166 | response.Write([]byte("hello")) 167 | proxyResponse, err := response.GetProxyResponse() 168 | Expect(err).To(BeNil()) 169 | 170 | // Headers are not also written to `Headers` field 171 | Expect(0).To(Equal(len(proxyResponse.Headers))) 172 | 173 | // There are two headers here because Content-Type is always written implicitly 174 | Expect(2).To(Equal(len(proxyResponse.MultiValueHeaders["Set-Cookie"]))) 175 | Expect("csrftoken=foobar").To(Equal(proxyResponse.MultiValueHeaders["Set-Cookie"][0])) 176 | Expect("session_id=barfoo").To(Equal(proxyResponse.MultiValueHeaders["Set-Cookie"][1])) 177 | }) 178 | }) 179 | 180 | }) 181 | -------------------------------------------------------------------------------- /core/response_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/base64" 5 | "math/rand" 6 | "net/http" 7 | "strings" 8 | 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("ResponseWriter tests", func() { 14 | Context("writing to response object", func() { 15 | response := NewProxyResponseWriter() 16 | 17 | It("Sets the correct default status", func() { 18 | Expect(defaultStatusCode).To(Equal(response.status)) 19 | }) 20 | 21 | It("Initializes the headers map", func() { 22 | Expect(response.headers).ToNot(BeNil()) 23 | Expect(0).To(Equal(len(response.headers))) 24 | }) 25 | 26 | It("Writes headers correctly", func() { 27 | response.Header().Add("Content-Type", "application/json") 28 | 29 | Expect(1).To(Equal(len(response.headers))) 30 | Expect("application/json").To(Equal(response.headers["Content-Type"][0])) 31 | }) 32 | 33 | It("Writes body content correctly", func() { 34 | binaryBody := make([]byte, 256) 35 | _, err := rand.Read(binaryBody) 36 | Expect(err).To(BeNil()) 37 | 38 | written, err := response.Write(binaryBody) 39 | Expect(err).To(BeNil()) 40 | Expect(len(binaryBody)).To(Equal(written)) 41 | }) 42 | 43 | It("Automatically set the status code to 200", func() { 44 | Expect(http.StatusOK).To(Equal(response.status)) 45 | }) 46 | 47 | It("Forces the status to a new code", func() { 48 | response.WriteHeader(http.StatusAccepted) 49 | Expect(http.StatusAccepted).To(Equal(response.status)) 50 | }) 51 | }) 52 | 53 | Context("Automatically set response content type", func() { 54 | xmlBodyContent := "ToveJaniReminderDon't forget me this weekend!" 55 | htmlBodyContent := " Title of the documentContent of the document......" 56 | It("Does not set the content type if it's already set", func() { 57 | resp := NewProxyResponseWriter() 58 | resp.Header().Add("Content-Type", "application/json") 59 | 60 | resp.Write([]byte(xmlBodyContent)) 61 | 62 | Expect("application/json").To(Equal(resp.Header().Get("Content-Type"))) 63 | proxyResp, err := resp.GetProxyResponse() 64 | Expect(err).To(BeNil()) 65 | Expect(1).To(Equal(len(proxyResp.MultiValueHeaders))) 66 | Expect("application/json").To(Equal(proxyResp.MultiValueHeaders["Content-Type"][0])) 67 | Expect(xmlBodyContent).To(Equal(proxyResp.Body)) 68 | }) 69 | 70 | It("Sets the content type to text/xml given the body", func() { 71 | resp := NewProxyResponseWriter() 72 | resp.Write([]byte(xmlBodyContent)) 73 | 74 | Expect("").ToNot(Equal(resp.Header().Get("Content-Type"))) 75 | Expect(true).To(Equal(strings.HasPrefix(resp.Header().Get("Content-Type"), "text/xml;"))) 76 | proxyResp, err := resp.GetProxyResponse() 77 | Expect(err).To(BeNil()) 78 | Expect(1).To(Equal(len(proxyResp.MultiValueHeaders))) 79 | Expect(true).To(Equal(strings.HasPrefix(proxyResp.MultiValueHeaders["Content-Type"][0], "text/xml;"))) 80 | Expect(xmlBodyContent).To(Equal(proxyResp.Body)) 81 | }) 82 | 83 | It("Sets the content type to text/html given the body", func() { 84 | resp := NewProxyResponseWriter() 85 | resp.Write([]byte(htmlBodyContent)) 86 | 87 | Expect("").ToNot(Equal(resp.Header().Get("Content-Type"))) 88 | Expect(true).To(Equal(strings.HasPrefix(resp.Header().Get("Content-Type"), "text/html;"))) 89 | proxyResp, err := resp.GetProxyResponse() 90 | Expect(err).To(BeNil()) 91 | Expect(1).To(Equal(len(proxyResp.MultiValueHeaders))) 92 | Expect(true).To(Equal(strings.HasPrefix(proxyResp.MultiValueHeaders["Content-Type"][0], "text/html;"))) 93 | Expect(htmlBodyContent).To(Equal(proxyResp.Body)) 94 | }) 95 | }) 96 | 97 | Context("Export API Gateway proxy response", func() { 98 | emtpyResponse := NewProxyResponseWriter() 99 | emtpyResponse.Header().Add("Content-Type", "application/json") 100 | 101 | It("Refuses empty responses with default status code", func() { 102 | _, err := emtpyResponse.GetProxyResponse() 103 | Expect(err).ToNot(BeNil()) 104 | Expect("Status code not set on response").To(Equal(err.Error())) 105 | }) 106 | 107 | simpleResponse := NewProxyResponseWriter() 108 | simpleResponse.Write([]byte("hello")) 109 | simpleResponse.Header().Add("Content-Type", "text/plain") 110 | It("Writes text body correctly", func() { 111 | proxyResponse, err := simpleResponse.GetProxyResponse() 112 | Expect(err).To(BeNil()) 113 | Expect(proxyResponse).ToNot(BeNil()) 114 | 115 | Expect("hello").To(Equal(proxyResponse.Body)) 116 | Expect(http.StatusOK).To(Equal(proxyResponse.StatusCode)) 117 | Expect(1).To(Equal(len(proxyResponse.MultiValueHeaders))) 118 | Expect(true).To(Equal(strings.HasPrefix(proxyResponse.MultiValueHeaders["Content-Type"][0], "text/plain"))) 119 | Expect(proxyResponse.IsBase64Encoded).To(BeFalse()) 120 | }) 121 | 122 | binaryResponse := NewProxyResponseWriter() 123 | binaryResponse.Header().Add("Content-Type", "application/octet-stream") 124 | binaryBody := make([]byte, 256) 125 | _, err := rand.Read(binaryBody) 126 | if err != nil { 127 | Fail("Could not generate random binary body") 128 | } 129 | binaryResponse.Write(binaryBody) 130 | binaryResponse.WriteHeader(http.StatusAccepted) 131 | 132 | It("Encodes binary responses correctly", func() { 133 | proxyResponse, err := binaryResponse.GetProxyResponse() 134 | Expect(err).To(BeNil()) 135 | Expect(proxyResponse).ToNot(BeNil()) 136 | 137 | Expect(proxyResponse.IsBase64Encoded).To(BeTrue()) 138 | Expect(base64.StdEncoding.EncodedLen(len(binaryBody))).To(Equal(len(proxyResponse.Body))) 139 | 140 | Expect(base64.StdEncoding.EncodeToString(binaryBody)).To(Equal(proxyResponse.Body)) 141 | Expect(1).To(Equal(len(proxyResponse.MultiValueHeaders))) 142 | Expect("application/octet-stream").To(Equal(proxyResponse.MultiValueHeaders["Content-Type"][0])) 143 | Expect(http.StatusAccepted).To(Equal(proxyResponse.StatusCode)) 144 | }) 145 | }) 146 | 147 | Context("Handle multi-value headers", func() { 148 | 149 | It("Writes single-value headers correctly", func() { 150 | response := NewProxyResponseWriter() 151 | response.Header().Add("Content-Type", "application/json") 152 | response.Write([]byte("hello")) 153 | proxyResponse, err := response.GetProxyResponse() 154 | Expect(err).To(BeNil()) 155 | 156 | // Headers are not also written to `Headers` field 157 | Expect(0).To(Equal(len(proxyResponse.Headers))) 158 | Expect(1).To(Equal(len(proxyResponse.MultiValueHeaders["Content-Type"]))) 159 | Expect("application/json").To(Equal(proxyResponse.MultiValueHeaders["Content-Type"][0])) 160 | }) 161 | 162 | It("Writes multi-value headers correctly", func() { 163 | response := NewProxyResponseWriter() 164 | response.Header().Add("Set-Cookie", "csrftoken=foobar") 165 | response.Header().Add("Set-Cookie", "session_id=barfoo") 166 | response.Write([]byte("hello")) 167 | proxyResponse, err := response.GetProxyResponse() 168 | Expect(err).To(BeNil()) 169 | 170 | // Headers are not also written to `Headers` field 171 | Expect(0).To(Equal(len(proxyResponse.Headers))) 172 | 173 | // There are two headers here because Content-Type is always written implicitly 174 | Expect(2).To(Equal(len(proxyResponse.MultiValueHeaders["Set-Cookie"]))) 175 | Expect("csrftoken=foobar").To(Equal(proxyResponse.MultiValueHeaders["Set-Cookie"][0])) 176 | Expect("session_id=barfoo").To(Equal(proxyResponse.MultiValueHeaders["Set-Cookie"][1])) 177 | }) 178 | }) 179 | 180 | }) 181 | -------------------------------------------------------------------------------- /core/responsev2.go: -------------------------------------------------------------------------------- 1 | // Package core provides utility methods that help convert proxy events 2 | // into an http.Request and http.ResponseWriter 3 | package core 4 | 5 | import ( 6 | "bytes" 7 | "encoding/base64" 8 | "errors" 9 | "net/http" 10 | "strings" 11 | "unicode/utf8" 12 | 13 | "github.com/aws/aws-lambda-go/events" 14 | ) 15 | 16 | // ProxyResponseWriterV2 implements http.ResponseWriter and adds the method 17 | // necessary to return an events.APIGatewayProxyResponse object 18 | type ProxyResponseWriterV2 struct { 19 | headers http.Header 20 | body bytes.Buffer 21 | status int 22 | observers []chan<- bool 23 | } 24 | 25 | // NewProxyResponseWriter returns a new ProxyResponseWriter object. 26 | // The object is initialized with an empty map of headers and a 27 | // status code of -1 28 | func NewProxyResponseWriterV2() *ProxyResponseWriterV2 { 29 | return &ProxyResponseWriterV2{ 30 | headers: make(http.Header), 31 | status: defaultStatusCode, 32 | observers: make([]chan<- bool, 0), 33 | } 34 | 35 | } 36 | 37 | func (r *ProxyResponseWriterV2) CloseNotify() <-chan bool { 38 | ch := make(chan bool, 1) 39 | 40 | r.observers = append(r.observers, ch) 41 | 42 | return ch 43 | } 44 | 45 | func (r *ProxyResponseWriterV2) notifyClosed() { 46 | for _, v := range r.observers { 47 | v <- true 48 | } 49 | } 50 | 51 | // Header implementation from the http.ResponseWriter interface. 52 | func (r *ProxyResponseWriterV2) Header() http.Header { 53 | return r.headers 54 | } 55 | 56 | // Write sets the response body in the object. If no status code 57 | // was set before with the WriteHeader method it sets the status 58 | // for the response to 200 OK. 59 | func (r *ProxyResponseWriterV2) Write(body []byte) (int, error) { 60 | if r.status == defaultStatusCode { 61 | r.status = http.StatusOK 62 | } 63 | 64 | // if the content type header is not set when we write the body we try to 65 | // detect one and set it by default. If the content type cannot be detected 66 | // it is automatically set to "application/octet-stream" by the 67 | // DetectContentType method 68 | if r.Header().Get(contentTypeHeaderKey) == "" { 69 | r.Header().Add(contentTypeHeaderKey, http.DetectContentType(body)) 70 | } 71 | 72 | return (&r.body).Write(body) 73 | } 74 | 75 | // WriteHeader sets a status code for the response. This method is used 76 | // for error responses. 77 | func (r *ProxyResponseWriterV2) WriteHeader(status int) { 78 | r.status = status 79 | } 80 | 81 | // GetProxyResponse converts the data passed to the response writer into 82 | // an events.APIGatewayProxyResponse object. 83 | // Returns a populated proxy response object. If the response is invalid, for example 84 | // has no headers or an invalid status code returns an error. 85 | func (r *ProxyResponseWriterV2) GetProxyResponse() (events.APIGatewayV2HTTPResponse, error) { 86 | r.notifyClosed() 87 | 88 | if r.status == defaultStatusCode { 89 | return events.APIGatewayV2HTTPResponse{}, errors.New("Status code not set on response") 90 | } 91 | 92 | var output string 93 | isBase64 := false 94 | 95 | bb := (&r.body).Bytes() 96 | 97 | if utf8.Valid(bb) { 98 | output = string(bb) 99 | } else { 100 | output = base64.StdEncoding.EncodeToString(bb) 101 | isBase64 = true 102 | } 103 | 104 | headers := make(map[string]string) 105 | cookies := make([]string, 0) 106 | 107 | for headerKey, headerValue := range http.Header(r.headers) { 108 | if strings.EqualFold("set-cookie", headerKey) { 109 | cookies = append(cookies, headerValue...) 110 | continue 111 | } 112 | headers[headerKey] = strings.Join(headerValue, ",") 113 | } 114 | 115 | return events.APIGatewayV2HTTPResponse{ 116 | StatusCode: r.status, 117 | Headers: headers, 118 | Body: output, 119 | IsBase64Encoded: isBase64, 120 | Cookies: cookies, 121 | }, nil 122 | } 123 | -------------------------------------------------------------------------------- /core/responsev2_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/base64" 5 | "math/rand" 6 | "net/http" 7 | "strings" 8 | 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("ResponseWriterV2 tests", func() { 14 | Context("writing to response object", func() { 15 | response := NewProxyResponseWriterV2() 16 | 17 | It("Sets the correct default status", func() { 18 | Expect(defaultStatusCode).To(Equal(response.status)) 19 | }) 20 | 21 | It("Initializes the headers map", func() { 22 | Expect(response.headers).ToNot(BeNil()) 23 | Expect(0).To(Equal(len(response.headers))) 24 | }) 25 | 26 | It("Writes headers correctly", func() { 27 | response.Header().Add("Content-Type", "application/json") 28 | 29 | Expect(1).To(Equal(len(response.headers))) 30 | Expect("application/json").To(Equal(response.headers["Content-Type"][0])) 31 | }) 32 | 33 | It("Writes body content correctly", func() { 34 | binaryBody := make([]byte, 256) 35 | _, err := rand.Read(binaryBody) 36 | Expect(err).To(BeNil()) 37 | 38 | written, err := response.Write(binaryBody) 39 | Expect(err).To(BeNil()) 40 | Expect(len(binaryBody)).To(Equal(written)) 41 | }) 42 | 43 | It("Automatically set the status code to 200", func() { 44 | Expect(http.StatusOK).To(Equal(response.status)) 45 | }) 46 | 47 | It("Forces the status to a new code", func() { 48 | response.WriteHeader(http.StatusAccepted) 49 | Expect(http.StatusAccepted).To(Equal(response.status)) 50 | }) 51 | }) 52 | 53 | Context("Automatically set response content type", func() { 54 | xmlBodyContent := "ToveJaniReminderDon't forget me this weekend!" 55 | htmlBodyContent := " Title of the documentContent of the document......" 56 | It("Does not set the content type if it's already set", func() { 57 | resp := NewProxyResponseWriterV2() 58 | resp.Header().Add("Content-Type", "application/json") 59 | 60 | resp.Write([]byte(xmlBodyContent)) 61 | 62 | Expect("application/json").To(Equal(resp.Header().Get("Content-Type"))) 63 | proxyResp, err := resp.GetProxyResponse() 64 | Expect(err).To(BeNil()) 65 | Expect(1).To(Equal(len(proxyResp.Headers))) 66 | Expect("application/json").To(Equal(proxyResp.Headers["Content-Type"])) 67 | Expect(xmlBodyContent).To(Equal(proxyResp.Body)) 68 | }) 69 | 70 | It("Sets the content type to text/xml given the body", func() { 71 | resp := NewProxyResponseWriterV2() 72 | resp.Write([]byte(xmlBodyContent)) 73 | 74 | Expect("").ToNot(Equal(resp.Header().Get("Content-Type"))) 75 | Expect(true).To(Equal(strings.HasPrefix(resp.Header().Get("Content-Type"), "text/xml;"))) 76 | proxyResp, err := resp.GetProxyResponse() 77 | Expect(err).To(BeNil()) 78 | Expect(1).To(Equal(len(proxyResp.Headers))) 79 | Expect(true).To(Equal(strings.HasPrefix(proxyResp.Headers["Content-Type"], "text/xml;"))) 80 | Expect(xmlBodyContent).To(Equal(proxyResp.Body)) 81 | }) 82 | 83 | It("Sets the content type to text/html given the body", func() { 84 | resp := NewProxyResponseWriterV2() 85 | resp.Write([]byte(htmlBodyContent)) 86 | 87 | Expect("").ToNot(Equal(resp.Header().Get("Content-Type"))) 88 | Expect(true).To(Equal(strings.HasPrefix(resp.Header().Get("Content-Type"), "text/html;"))) 89 | proxyResp, err := resp.GetProxyResponse() 90 | Expect(err).To(BeNil()) 91 | Expect(1).To(Equal(len(proxyResp.Headers))) 92 | Expect(true).To(Equal(strings.HasPrefix(proxyResp.Headers["Content-Type"], "text/html;"))) 93 | Expect(htmlBodyContent).To(Equal(proxyResp.Body)) 94 | }) 95 | }) 96 | 97 | Context("Export API Gateway proxy response", func() { 98 | emtpyResponse := NewProxyResponseWriterV2() 99 | emtpyResponse.Header().Add("Content-Type", "application/json") 100 | 101 | It("Refuses empty responses with default status code", func() { 102 | _, err := emtpyResponse.GetProxyResponse() 103 | Expect(err).ToNot(BeNil()) 104 | Expect("Status code not set on response").To(Equal(err.Error())) 105 | }) 106 | 107 | simpleResponse := NewProxyResponseWriterV2() 108 | simpleResponse.Write([]byte("hello")) 109 | simpleResponse.Header().Add("Content-Type", "text/plain") 110 | It("Writes text body correctly", func() { 111 | proxyResponse, err := simpleResponse.GetProxyResponse() 112 | Expect(err).To(BeNil()) 113 | Expect(proxyResponse).ToNot(BeNil()) 114 | 115 | Expect("hello").To(Equal(proxyResponse.Body)) 116 | Expect(http.StatusOK).To(Equal(proxyResponse.StatusCode)) 117 | Expect(1).To(Equal(len(proxyResponse.Headers))) 118 | Expect(true).To(Equal(strings.HasPrefix(proxyResponse.Headers["Content-Type"], "text/plain"))) 119 | Expect(proxyResponse.IsBase64Encoded).To(BeFalse()) 120 | }) 121 | 122 | binaryResponse := NewProxyResponseWriterV2() 123 | binaryResponse.Header().Add("Content-Type", "application/octet-stream") 124 | binaryBody := make([]byte, 256) 125 | _, err := rand.Read(binaryBody) 126 | if err != nil { 127 | Fail("Could not generate random binary body") 128 | } 129 | binaryResponse.Write(binaryBody) 130 | binaryResponse.WriteHeader(http.StatusAccepted) 131 | 132 | It("Encodes binary responses correctly", func() { 133 | proxyResponse, err := binaryResponse.GetProxyResponse() 134 | Expect(err).To(BeNil()) 135 | Expect(proxyResponse).ToNot(BeNil()) 136 | 137 | Expect(proxyResponse.IsBase64Encoded).To(BeTrue()) 138 | Expect(base64.StdEncoding.EncodedLen(len(binaryBody))).To(Equal(len(proxyResponse.Body))) 139 | 140 | Expect(base64.StdEncoding.EncodeToString(binaryBody)).To(Equal(proxyResponse.Body)) 141 | Expect(1).To(Equal(len(proxyResponse.Headers))) 142 | Expect("application/octet-stream").To(Equal(proxyResponse.Headers["Content-Type"])) 143 | Expect(http.StatusAccepted).To(Equal(proxyResponse.StatusCode)) 144 | }) 145 | }) 146 | 147 | Context("Handle multi-value headers", func() { 148 | 149 | It("Writes single-value headers correctly", func() { 150 | response := NewProxyResponseWriterV2() 151 | response.Header().Add("Content-Type", "application/json") 152 | response.Write([]byte("hello")) 153 | proxyResponse, err := response.GetProxyResponse() 154 | Expect(err).To(BeNil()) 155 | 156 | Expect(1).To(Equal(len(proxyResponse.Headers))) 157 | Expect("application/json").To(Equal(proxyResponse.Headers["Content-Type"])) 158 | }) 159 | 160 | It("Writes multi-value headers correctly", func() { 161 | response := NewProxyResponseWriterV2() 162 | response.Header().Add("Accepts", "foobar") 163 | response.Header().Add("Accepts", "barfoo") 164 | response.Write([]byte("hello")) 165 | proxyResponse, err := response.GetProxyResponse() 166 | Expect(err).To(BeNil()) 167 | 168 | Expect(2).To(Equal(len(proxyResponse.Headers))) 169 | Expect("foobar,barfoo").To(Equal(proxyResponse.Headers["Accepts"])) 170 | }) 171 | 172 | It("Writes cookies correctly", func() { 173 | response := NewProxyResponseWriterV2() 174 | response.Header().Add("Set-Cookie", "csrftoken=foobar") 175 | response.Header().Add("Set-Cookie", "session_id=barfoo") 176 | response.Write([]byte("hello")) 177 | proxyResponse, err := response.GetProxyResponse() 178 | Expect(err).To(BeNil()) 179 | 180 | Expect(2).To(Equal(len(proxyResponse.Cookies))) 181 | Expect(strings.Split("csrftoken=foobar,session_id=barfoo", ",")).To(Equal(proxyResponse.Cookies)) 182 | }) 183 | }) 184 | 185 | }) 186 | -------------------------------------------------------------------------------- /core/switchablerequest.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/aws/aws-lambda-go/events" 7 | ) 8 | 9 | type SwitchableAPIGatewayRequest struct { 10 | v interface{} // v is Always nil, or a pointer of APIGatewayProxyRequest or APIGatewayV2HTTPRequest 11 | } 12 | 13 | // NewSwitchableAPIGatewayRequestV1 creates a new SwitchableAPIGatewayRequest from APIGatewayProxyRequest 14 | func NewSwitchableAPIGatewayRequestV1(v *events.APIGatewayProxyRequest) *SwitchableAPIGatewayRequest { 15 | return &SwitchableAPIGatewayRequest{ 16 | v: v, 17 | } 18 | } 19 | // NewSwitchableAPIGatewayRequestV2 creates a new SwitchableAPIGatewayRequest from APIGatewayV2HTTPRequest 20 | func NewSwitchableAPIGatewayRequestV2(v *events.APIGatewayV2HTTPRequest) *SwitchableAPIGatewayRequest { 21 | return &SwitchableAPIGatewayRequest{ 22 | v: v, 23 | } 24 | } 25 | 26 | // MarshalJSON is a pass through serialization 27 | func (s *SwitchableAPIGatewayRequest) MarshalJSON() ([]byte, error) { 28 | return json.Marshal(s.v) 29 | } 30 | 31 | // UnmarshalJSON is a switching serialization based on the presence of fields in the 32 | // source JSON, multiValueQueryStringParameters for APIGatewayProxyRequest and rawQueryString for 33 | // APIGatewayV2HTTPRequest. 34 | func (s *SwitchableAPIGatewayRequest) UnmarshalJSON(b []byte) error { 35 | delta := map[string]json.RawMessage{} 36 | if err := json.Unmarshal(b, &delta); err != nil { 37 | return err 38 | } 39 | _, v1test := delta["multiValueQueryStringParameters"] 40 | _, v2test := delta["rawQueryString"] 41 | s.v = nil 42 | if v1test && !v2test { 43 | s.v = &events.APIGatewayProxyRequest{} 44 | } else if !v1test && v2test { 45 | s.v = &events.APIGatewayV2HTTPRequest{} 46 | } else { 47 | return errors.New("unable to determine request version") 48 | } 49 | return json.Unmarshal(b, s.v) 50 | } 51 | 52 | // Version1 returns the contained events.APIGatewayProxyRequest or nil 53 | func (s *SwitchableAPIGatewayRequest) Version1() *events.APIGatewayProxyRequest { 54 | switch v := s.v.(type) { 55 | case *events.APIGatewayProxyRequest: 56 | return v 57 | case events.APIGatewayProxyRequest: 58 | return &v 59 | } 60 | return nil 61 | } 62 | 63 | // Version2 returns the contained events.APIGatewayV2HTTPRequest or nil 64 | func (s *SwitchableAPIGatewayRequest) Version2() *events.APIGatewayV2HTTPRequest { 65 | switch v := s.v.(type) { 66 | case *events.APIGatewayV2HTTPRequest: 67 | return v 68 | case events.APIGatewayV2HTTPRequest: 69 | return &v 70 | } 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /core/switchablerequest_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/aws/aws-lambda-go/events" 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("SwitchableAPIGatewayRequest", func() { 11 | Context("Serialization", func() { 12 | It("v1 serialized okay", func() { 13 | e := NewSwitchableAPIGatewayRequestV1(&events.APIGatewayProxyRequest{ 14 | MultiValueQueryStringParameters: map[string][]string{}, 15 | }) 16 | b, err := json.Marshal(e) 17 | Expect(err).To(BeNil()) 18 | m := map[string]interface{}{} 19 | err = json.Unmarshal(b, &m) 20 | Expect(err).To(BeNil()) 21 | Expect(m["multiValueQueryStringParameters"]).To(Equal(map[string]interface {}{})) 22 | Expect(m["body"]).To(Equal("")) 23 | }) 24 | It("v2 serialized okay", func() { 25 | e := NewSwitchableAPIGatewayRequestV2(&events.APIGatewayV2HTTPRequest{}) 26 | b, err := json.Marshal(e) 27 | Expect(err).To(BeNil()) 28 | m := map[string]interface{}{} 29 | err = json.Unmarshal(b, &m) 30 | Expect(err).To(BeNil()) 31 | Expect(m["rawQueryString"]).To(Equal("")) 32 | Expect(m["isBase64Encoded"]).To(Equal(false)) 33 | }) 34 | }) 35 | Context("Deserialization", func() { 36 | It("v1 deserialized okay", func() { 37 | input := &events.APIGatewayProxyRequest{ 38 | Body: "234", 39 | MultiValueQueryStringParameters: map[string][]string{ 40 | "Test": []string{ "Value1", "Value2", }, 41 | }, 42 | } 43 | b, _ := json.Marshal(input) 44 | s := SwitchableAPIGatewayRequest{} 45 | err := s.UnmarshalJSON(b) 46 | Expect(err).To(BeNil()) 47 | Expect(s.Version2()).To(BeNil()) 48 | Expect(s.Version1()).To(BeEquivalentTo(input)) 49 | }) 50 | It("v2 deserialized okay", func() { 51 | input := &events.APIGatewayV2HTTPRequest{ 52 | IsBase64Encoded: true, 53 | RawQueryString: "a=b&c=d", 54 | } 55 | b, _ := json.Marshal(input) 56 | s := SwitchableAPIGatewayRequest{} 57 | err := s.UnmarshalJSON(b) 58 | Expect(err).To(BeNil()) 59 | Expect(s.Version1()).To(BeNil()) 60 | Expect(s.Version2()).To(BeEquivalentTo(input)) 61 | }) 62 | })}) 63 | 64 | func getProxyRequestV2(path string, method string) events.APIGatewayV2HTTPRequest { 65 | return events.APIGatewayV2HTTPRequest{ 66 | RequestContext: events.APIGatewayV2HTTPRequestContext{ 67 | HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{ 68 | Path: path, 69 | Method: method, 70 | }, 71 | }, 72 | RawPath: path, 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /core/switchableresponse.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/aws/aws-lambda-go/events" 7 | ) 8 | 9 | // SwitchableAPIGatewayResponse is a container for an APIGatewayProxyResponse or an APIGatewayV2HTTPResponse object which 10 | // handles serialization and deserialization and switching between the entities based on the presence of fields in the 11 | // source JSON, multiValueQueryStringParameters for APIGatewayProxyResponse and rawQueryString for 12 | // APIGatewayV2HTTPResponse. It also provides some simple switching functions (wrapped type switching.) 13 | type SwitchableAPIGatewayResponse struct { 14 | v interface{} 15 | } 16 | 17 | // NewSwitchableAPIGatewayResponseV1 creates a new SwitchableAPIGatewayResponse from APIGatewayProxyResponse 18 | func NewSwitchableAPIGatewayResponseV1(v *events.APIGatewayProxyResponse) *SwitchableAPIGatewayResponse { 19 | return &SwitchableAPIGatewayResponse{ 20 | v: v, 21 | } 22 | } 23 | 24 | // NewSwitchableAPIGatewayResponseV2 creates a new SwitchableAPIGatewayResponse from APIGatewayV2HTTPResponse 25 | func NewSwitchableAPIGatewayResponseV2(v *events.APIGatewayV2HTTPResponse) *SwitchableAPIGatewayResponse { 26 | return &SwitchableAPIGatewayResponse{ 27 | v: v, 28 | } 29 | } 30 | 31 | // MarshalJSON is a pass through serialization 32 | func (s *SwitchableAPIGatewayResponse) MarshalJSON() ([]byte, error) { 33 | return json.Marshal(s.v) 34 | } 35 | 36 | // UnmarshalJSON is a switching serialization based on the presence of fields in the 37 | // source JSON, statusCode to verify that it's either APIGatewayProxyResponse or APIGatewayV2HTTPResponse and then 38 | // rawQueryString for to determine if it is APIGatewayV2HTTPResponse or not. 39 | func (s *SwitchableAPIGatewayResponse) UnmarshalJSON(b []byte) error { 40 | delta := map[string]json.RawMessage{} 41 | if err := json.Unmarshal(b, &delta); err != nil { 42 | return err 43 | } 44 | _, test := delta["statusCode"] 45 | _, v2test := delta["cookies"] 46 | s.v = nil 47 | if test && !v2test { 48 | s.v = &events.APIGatewayProxyResponse{} 49 | } else if test && v2test { 50 | s.v = &events.APIGatewayV2HTTPResponse{} 51 | } else { 52 | return errors.New("unable to determine response version") 53 | } 54 | return json.Unmarshal(b, s.v) 55 | } 56 | 57 | // Version1 returns the contained events.APIGatewayProxyResponse or nil 58 | func (s *SwitchableAPIGatewayResponse) Version1() *events.APIGatewayProxyResponse { 59 | switch v := s.v.(type) { 60 | case *events.APIGatewayProxyResponse: 61 | return v 62 | case events.APIGatewayProxyResponse: 63 | return &v 64 | } 65 | return nil 66 | } 67 | 68 | // Version2 returns the contained events.APIGatewayV2HTTPResponse or nil 69 | func (s *SwitchableAPIGatewayResponse) Version2() *events.APIGatewayV2HTTPResponse { 70 | switch v := s.v.(type) { 71 | case *events.APIGatewayV2HTTPResponse: 72 | return v 73 | case events.APIGatewayV2HTTPResponse: 74 | return &v 75 | } 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /core/switchableresponse_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/aws/aws-lambda-go/events" 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("SwitchableAPIGatewayResponse", func() { 11 | Context("Serialization", func() { 12 | It("v1 serialized okay", func() { 13 | e := NewSwitchableAPIGatewayResponseV1(&events.APIGatewayProxyResponse{}) 14 | b, err := json.Marshal(e) 15 | Expect(err).To(BeNil()) 16 | m := map[string]interface{}{} 17 | err = json.Unmarshal(b, &m) 18 | Expect(err).To(BeNil()) 19 | Expect(m["statusCode"]).To(Equal(0.0)) 20 | Expect(m["body"]).To(Equal("")) 21 | }) 22 | It("v2 serialized okay", func() { 23 | e := NewSwitchableAPIGatewayResponseV2(&events.APIGatewayV2HTTPResponse{}) 24 | b, err := json.Marshal(e) 25 | Expect(err).To(BeNil()) 26 | m := map[string]interface{}{} 27 | err = json.Unmarshal(b, &m) 28 | Expect(err).To(BeNil()) 29 | Expect(m["statusCode"]).To(Equal(0.0)) 30 | Expect(m["body"]).To(Equal("")) 31 | }) 32 | }) 33 | Context("Deserialization", func() { 34 | It("v1 deserialized okay", func() { 35 | input := &events.APIGatewayProxyResponse{ 36 | StatusCode: 123, 37 | Body: "234", 38 | } 39 | b, _ := json.Marshal(input) 40 | s := SwitchableAPIGatewayResponse{} 41 | err := s.UnmarshalJSON(b) 42 | Expect(err).To(BeNil()) 43 | Expect(s.Version2()).To(BeNil()) 44 | Expect(s.Version1()).To(BeEquivalentTo(input)) 45 | }) 46 | It("v2 deserialized okay", func() { 47 | input := &events.APIGatewayV2HTTPResponse{ 48 | StatusCode: 123, 49 | Body: "234", 50 | Cookies: []string{"4", "5"}, 51 | } 52 | b, _ := json.Marshal(input) 53 | s := SwitchableAPIGatewayResponse{} 54 | err := s.UnmarshalJSON(b) 55 | Expect(err).To(BeNil()) 56 | Expect(s.Version1()).To(BeNil()) 57 | Expect(s.Version2()).To(BeEquivalentTo(input)) 58 | }) 59 | }) 60 | }) 61 | 62 | -------------------------------------------------------------------------------- /core/types.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/aws/aws-lambda-go/events" 8 | ) 9 | 10 | // GatewayTimeout returns a dafault Gateway Timeout (504) response 11 | func GatewayTimeout() events.APIGatewayProxyResponse { 12 | return events.APIGatewayProxyResponse{StatusCode: http.StatusGatewayTimeout} 13 | } 14 | 15 | // NewLoggedError generates a new error and logs it to stdout 16 | func NewLoggedError(format string, a ...interface{}) error { 17 | err := fmt.Errorf(format, a...) 18 | fmt.Println(err.Error()) 19 | return err 20 | } 21 | -------------------------------------------------------------------------------- /core/typesALB.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/aws/aws-lambda-go/events" 7 | ) 8 | 9 | func GatewayTimeoutALB() events.ALBTargetGroupResponse { 10 | return events.ALBTargetGroupResponse{StatusCode: http.StatusGatewayTimeout} 11 | } 12 | -------------------------------------------------------------------------------- /core/typesv2.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/aws/aws-lambda-go/events" 7 | ) 8 | 9 | func GatewayTimeoutV2() events.APIGatewayV2HTTPResponse { 10 | return events.APIGatewayV2HTTPResponse{StatusCode: http.StatusGatewayTimeout} 11 | } 12 | -------------------------------------------------------------------------------- /echo/adapter.go: -------------------------------------------------------------------------------- 1 | // Packge echolambda add Echo support for the aws-severless-go-api library. 2 | // Uses the core package behind the scenes and exposes the New method to 3 | // get a new instance and Proxy method to send request to the echo.Echo 4 | package echoadapter 5 | 6 | import ( 7 | "context" 8 | "net/http" 9 | 10 | "github.com/aws/aws-lambda-go/events" 11 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 12 | "github.com/labstack/echo/v4" 13 | ) 14 | 15 | // EchoLambda makes it easy to send API Gateway proxy events to a echo.Echo. 16 | // The library transforms the proxy event into an HTTP request and then 17 | // creates a proxy response object from the http.ResponseWriter 18 | type EchoLambda struct { 19 | core.RequestAccessor 20 | 21 | Echo *echo.Echo 22 | } 23 | 24 | // New creates a new instance of the EchoLambda object. 25 | // Receives an initialized *echo.Echo object - normally created with echo.New(). 26 | // It returns the initialized instance of the EchoLambda object. 27 | func New(e *echo.Echo) *EchoLambda { 28 | return &EchoLambda{Echo: e} 29 | } 30 | 31 | // Proxy receives an API Gateway proxy event, transforms it into an http.Request 32 | // object, and sends it to the echo.Echo for routing. 33 | // It returns a proxy response object generated from the http.ResponseWriter. 34 | func (e *EchoLambda) Proxy(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 35 | echoRequest, err := e.ProxyEventToHTTPRequest(req) 36 | return e.proxyInternal(echoRequest, err) 37 | } 38 | 39 | // ProxyWithContext receives context and an API Gateway proxy event, 40 | // transforms them into an http.Request object, and sends it to the echo.Echo for routing. 41 | // It returns a proxy response object generated from the http.ResponseWriter. 42 | func (e *EchoLambda) ProxyWithContext(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 43 | echoRequest, err := e.EventToRequestWithContext(ctx, req) 44 | return e.proxyInternal(echoRequest, err) 45 | } 46 | 47 | func (e *EchoLambda) proxyInternal(req *http.Request, err error) (events.APIGatewayProxyResponse, error) { 48 | 49 | if err != nil { 50 | return core.GatewayTimeout(), core.NewLoggedError("Could not convert proxy event to request: %v", err) 51 | } 52 | 53 | respWriter := core.NewProxyResponseWriter() 54 | e.Echo.ServeHTTP(http.ResponseWriter(respWriter), req) 55 | 56 | proxyResponse, err := respWriter.GetProxyResponse() 57 | if err != nil { 58 | return core.GatewayTimeout(), core.NewLoggedError("Error while generating proxy response: %v", err) 59 | } 60 | 61 | return proxyResponse, nil 62 | } 63 | -------------------------------------------------------------------------------- /echo/adapterALB.go: -------------------------------------------------------------------------------- 1 | package echoadapter 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/aws/aws-lambda-go/events" 8 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | // EchoLambdaALB makes it easy to send ALB proxy events to a echo.Echo. 13 | // The library transforms the proxy event into an HTTP request and then 14 | // creates a proxy response object from the http.ResponseWriter 15 | type EchoLambdaALB struct { 16 | core.RequestAccessorALB 17 | 18 | Echo *echo.Echo 19 | } 20 | 21 | // NewAPI creates a new instance of the EchoLambdaAPI object. 22 | // Receives an initialized *echo.Echo object - normally created with echo.New(). 23 | // It returns the initialized instance of the EchoLambdaALB object. 24 | func NewALB(e *echo.Echo) *EchoLambdaALB { 25 | return &EchoLambdaALB{Echo: e} 26 | } 27 | 28 | // Proxy receives an ALB event, transforms it into an http.Request 29 | // object, and sends it to the echo.Echo for routing. 30 | // It returns a proxy response object generated from the http.ResponseWriter. 31 | func (e *EchoLambdaALB) Proxy(req events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) { 32 | echoRequest, err := e.ProxyEventToHTTPRequest(req) 33 | return e.proxyInternal(echoRequest, err) 34 | } 35 | 36 | // ProxyWithContext receives context and an ALB event, 37 | // transforms them into an http.Request object, and sends it to the echo.Echo for routing. 38 | // It returns a proxy response object generated from the http.ResponseWriter. 39 | func (e *EchoLambdaALB) ProxyWithContext(ctx context.Context, req events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) { 40 | echoRequest, err := e.EventToRequestWithContext(ctx, req) 41 | return e.proxyInternal(echoRequest, err) 42 | } 43 | 44 | func (e *EchoLambdaALB) proxyInternal(req *http.Request, err error) (events.ALBTargetGroupResponse, error) { 45 | 46 | if err != nil { 47 | return core.GatewayTimeoutALB(), core.NewLoggedError("Could not convert proxy event to request: %v", err) 48 | } 49 | 50 | respWriter := core.NewProxyResponseWriterALB() 51 | e.Echo.ServeHTTP(http.ResponseWriter(respWriter), req) 52 | 53 | proxyResponse, err := respWriter.GetProxyResponse() 54 | if err != nil { 55 | return core.GatewayTimeoutALB(), core.NewLoggedError("Error while generating proxy response: %v", err) 56 | } 57 | 58 | return proxyResponse, nil 59 | } 60 | -------------------------------------------------------------------------------- /echo/adapterv2.go: -------------------------------------------------------------------------------- 1 | package echoadapter 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/aws/aws-lambda-go/events" 8 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 9 | "github.com/labstack/echo/v4" 10 | ) 11 | 12 | // EchoLambdaV2 makes it easy to send API Gateway proxy V2 events to a echo.Echo. 13 | // The library transforms the proxy event into an HTTP request and then 14 | // creates a proxy response object from the http.ResponseWriter 15 | type EchoLambdaV2 struct { 16 | core.RequestAccessorV2 17 | 18 | Echo *echo.Echo 19 | } 20 | 21 | // NewV2 creates a new instance of the EchoLambda object. 22 | // Receives an initialized *echo.Echo object - normally created with echo.New(). 23 | // It returns the initialized instance of the EchoLambdaV2 object. 24 | func NewV2(e *echo.Echo) *EchoLambdaV2 { 25 | return &EchoLambdaV2{Echo: e} 26 | } 27 | 28 | // Proxy receives an API Gateway proxy V2 event, transforms it into an http.Request 29 | // object, and sends it to the echo.Echo for routing. 30 | // It returns a proxy response object generated from the http.ResponseWriter. 31 | func (e *EchoLambdaV2) Proxy(req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 32 | echoRequest, err := e.ProxyEventToHTTPRequest(req) 33 | return e.proxyInternal(echoRequest, err) 34 | } 35 | 36 | // ProxyWithContext receives context and an API Gateway proxy V2 event, 37 | // transforms them into an http.Request object, and sends it to the echo.Echo for routing. 38 | // It returns a proxy response object generated from the http.ResponseWriter. 39 | func (e *EchoLambdaV2) ProxyWithContext(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 40 | echoRequest, err := e.EventToRequestWithContext(ctx, req) 41 | return e.proxyInternal(echoRequest, err) 42 | } 43 | 44 | func (e *EchoLambdaV2) proxyInternal(req *http.Request, err error) (events.APIGatewayV2HTTPResponse, error) { 45 | 46 | if err != nil { 47 | return core.GatewayTimeoutV2(), core.NewLoggedError("Could not convert proxy event to request: %v", err) 48 | } 49 | 50 | respWriter := core.NewProxyResponseWriterV2() 51 | e.Echo.ServeHTTP(http.ResponseWriter(respWriter), req) 52 | 53 | proxyResponse, err := respWriter.GetProxyResponse() 54 | if err != nil { 55 | return core.GatewayTimeoutV2(), core.NewLoggedError("Error while generating proxy response: %v", err) 56 | } 57 | 58 | return proxyResponse, nil 59 | } 60 | -------------------------------------------------------------------------------- /echo/echo_suite_test.go: -------------------------------------------------------------------------------- 1 | package echoadapter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestEcho(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Echo Suite") 13 | } 14 | -------------------------------------------------------------------------------- /echo/echolambda_test.go: -------------------------------------------------------------------------------- 1 | package echoadapter_test 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/aws/aws-lambda-go/events" 7 | echoadapter "github.com/awslabs/aws-lambda-go-api-proxy/echo" 8 | "github.com/labstack/echo/v4" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("EchoLambda tests", func() { 15 | Context("Simple ping request", func() { 16 | It("Proxies the event correctly", func() { 17 | log.Println("Starting test") 18 | e := echo.New() 19 | e.GET("/ping", func(c echo.Context) error { 20 | log.Println("Handler!!") 21 | return c.String(200, "pong") 22 | }) 23 | 24 | adapter := echoadapter.New(e) 25 | 26 | req := events.APIGatewayProxyRequest{ 27 | Path: "/ping", 28 | HTTPMethod: "GET", 29 | } 30 | 31 | resp, err := adapter.Proxy(req) 32 | 33 | Expect(err).To(BeNil()) 34 | Expect(resp.StatusCode).To(Equal(200)) 35 | }) 36 | }) 37 | }) 38 | 39 | var _ = Describe("EchoLambdaV2 tests", func() { 40 | Context("Simple ping request", func() { 41 | It("Proxies the event correctly", func() { 42 | log.Println("Starting test") 43 | e := echo.New() 44 | e.GET("/ping", func(c echo.Context) error { 45 | log.Println("Handler!!") 46 | return c.String(200, "pong") 47 | }) 48 | 49 | adapter := echoadapter.NewV2(e) 50 | 51 | req := events.APIGatewayV2HTTPRequest{ 52 | RequestContext: events.APIGatewayV2HTTPRequestContext{ 53 | HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{ 54 | Method: "GET", 55 | Path: "/ping", 56 | }, 57 | }, 58 | } 59 | 60 | resp, err := adapter.Proxy(req) 61 | 62 | Expect(err).To(BeNil()) 63 | Expect(resp.StatusCode).To(Equal(200)) 64 | }) 65 | }) 66 | }) 67 | 68 | var _ = Describe("EchoLambdaALB tests", func() { 69 | Context("Simple ping request", func() { 70 | It("Proxies the event correctly", func() { 71 | log.Println("Starting test") 72 | e := echo.New() 73 | e.GET("/ping", func(c echo.Context) error { 74 | log.Println("Handler!!") 75 | return c.String(200, "pong") 76 | }) 77 | 78 | adapter := echoadapter.NewALB(e) 79 | 80 | req := events.ALBTargetGroupRequest{ 81 | HTTPMethod: "GET", 82 | Path: "/ping", 83 | RequestContext: events.ALBTargetGroupRequestContext{ 84 | ELB: events.ELBContext{TargetGroupArn: " ad"}, 85 | }} 86 | 87 | resp, err := adapter.Proxy(req) 88 | 89 | Expect(err).To(BeNil()) 90 | Expect(resp.StatusCode).To(Equal(200)) 91 | }) 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /examples/fiber/main.go: -------------------------------------------------------------------------------- 1 | // main.go 2 | package main 3 | 4 | import ( 5 | "context" 6 | "log" 7 | 8 | "github.com/aws/aws-lambda-go/events" 9 | "github.com/aws/aws-lambda-go/lambda" 10 | fiberadapter "github.com/awslabs/aws-lambda-go-api-proxy/fiber" 11 | "github.com/gofiber/fiber/v2" 12 | ) 13 | 14 | var fiberLambda *fiberadapter.FiberLambda 15 | 16 | // init the Fiber Server 17 | func init() { 18 | log.Printf("Fiber cold start") 19 | var app *fiber.App 20 | app = fiber.New() 21 | 22 | app.Get("/", func(c *fiber.Ctx) error { 23 | return c.SendString("Hello, World!") 24 | }) 25 | 26 | fiberLambda = fiberadapter.New(app) 27 | } 28 | 29 | // Handler will deal with Fiber working with Lambda 30 | func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 31 | // If no name is provided in the HTTP request body, throw an error 32 | return fiberLambda.ProxyWithContext(ctx, req) 33 | } 34 | 35 | func main() { 36 | // Make the handler available for Remote Procedure Call by AWS Lambda 37 | lambda.Start(Handler) 38 | } 39 | -------------------------------------------------------------------------------- /fiber/adapter.go: -------------------------------------------------------------------------------- 1 | // Package fiberadapter adds Fiber support for the aws-severless-go-api library. 2 | // Uses the core package behind the scenes and exposes the New method to 3 | // get a new instance and Proxy method to send request to the Fiber app. 4 | package fiberadapter 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "io" 10 | "log" 11 | "net" 12 | "net/http" 13 | "strings" 14 | 15 | "github.com/aws/aws-lambda-go/events" 16 | "github.com/gofiber/fiber/v2" 17 | "github.com/gofiber/fiber/v2/utils" 18 | "github.com/valyala/fasthttp" 19 | 20 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 21 | ) 22 | 23 | // FiberLambda makes it easy to send API Gateway proxy events to a fiber.App. 24 | // The library transforms the proxy event into an HTTP request and then 25 | // creates a proxy response object from the *fiber.Ctx 26 | type FiberLambda struct { 27 | core.RequestAccessor 28 | v2 core.RequestAccessorV2 29 | app *fiber.App 30 | } 31 | 32 | // New creates a new instance of the FiberLambda object. 33 | // Receives an initialized *fiber.App object - normally created with fiber.New(). 34 | // It returns the initialized instance of the FiberLambda object. 35 | func New(app *fiber.App) *FiberLambda { 36 | return &FiberLambda{ 37 | app: app, 38 | } 39 | } 40 | 41 | // Proxy receives an API Gateway proxy event, transforms it into an http.Request 42 | // object, and sends it to the fiber.App for routing. 43 | // It returns a proxy response object generated from the http.ResponseWriter. 44 | func (f *FiberLambda) Proxy(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 45 | fiberRequest, err := f.ProxyEventToHTTPRequest(req) 46 | return f.proxyInternal(fiberRequest, err) 47 | } 48 | 49 | // ProxyV2 is just same as Proxy() but for APIGateway HTTP payload v2 50 | func (f *FiberLambda) ProxyV2(req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 51 | fiberRequest, err := f.v2.ProxyEventToHTTPRequest(req) 52 | return f.proxyInternalV2(fiberRequest, err) 53 | } 54 | 55 | // ProxyWithContext receives context and an API Gateway proxy event, 56 | // transforms them into an http.Request object, and sends it to the echo.Echo for routing. 57 | // It returns a proxy response object generated from the http.ResponseWriter. 58 | func (f *FiberLambda) ProxyWithContext(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 59 | fiberRequest, err := f.EventToRequestWithContext(ctx, req) 60 | return f.proxyInternal(fiberRequest, err) 61 | } 62 | 63 | // ProxyWithContextV2 is just same as ProxyWithContext() but for APIGateway HTTP payload v2 64 | func (f *FiberLambda) ProxyWithContextV2(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 65 | fiberRequest, err := f.v2.EventToRequestWithContext(ctx, req) 66 | return f.proxyInternalV2(fiberRequest, err) 67 | } 68 | 69 | func (f *FiberLambda) proxyInternal(req *http.Request, err error) (events.APIGatewayProxyResponse, error) { 70 | 71 | if err != nil { 72 | return core.GatewayTimeout(), core.NewLoggedError("Could not convert proxy event to request: %v", err) 73 | } 74 | 75 | resp := core.NewProxyResponseWriter() 76 | f.adaptor(resp, req) 77 | 78 | proxyResponse, err := resp.GetProxyResponse() 79 | if err != nil { 80 | return core.GatewayTimeout(), core.NewLoggedError("Error while generating proxy response: %v", err) 81 | } 82 | 83 | return proxyResponse, nil 84 | } 85 | 86 | func (f *FiberLambda) proxyInternalV2(req *http.Request, err error) (events.APIGatewayV2HTTPResponse, error) { 87 | 88 | if err != nil { 89 | return core.GatewayTimeoutV2(), core.NewLoggedError("Could not convert proxy event to request: %v", err) 90 | } 91 | 92 | resp := core.NewProxyResponseWriterV2() 93 | f.adaptor(resp, req) 94 | 95 | proxyResponse, err := resp.GetProxyResponse() 96 | if err != nil { 97 | return core.GatewayTimeoutV2(), core.NewLoggedError("Error while generating proxy response: %v", err) 98 | } 99 | 100 | return proxyResponse, nil 101 | } 102 | 103 | func (f *FiberLambda) adaptor(w http.ResponseWriter, r *http.Request) { 104 | // New fasthttp request 105 | req := fasthttp.AcquireRequest() 106 | defer fasthttp.ReleaseRequest(req) 107 | 108 | // Convert net/http -> fasthttp request 109 | body, err := io.ReadAll(r.Body) 110 | if err != nil { 111 | http.Error(w, utils.StatusMessage(fiber.StatusInternalServerError), fiber.StatusInternalServerError) 112 | return 113 | } 114 | req.Header.SetContentLength(len(body)) 115 | _, _ = req.BodyWriter().Write(body) 116 | 117 | req.Header.SetMethod(r.Method) 118 | req.SetRequestURI(r.RequestURI) 119 | req.SetHost(r.Host) 120 | for key, val := range r.Header { 121 | for _, v := range val { 122 | switch key { 123 | case fiber.HeaderHost, 124 | fiber.HeaderContentType, 125 | fiber.HeaderUserAgent, 126 | fiber.HeaderContentLength, 127 | fiber.HeaderConnection: 128 | req.Header.Set(key, v) 129 | default: 130 | req.Header.Add(key, v) 131 | } 132 | } 133 | } 134 | 135 | // We need to make sure the net.ResolveTCPAddr call works as it expects a port 136 | addrWithPort := r.RemoteAddr 137 | if !strings.Contains(r.RemoteAddr, ":") { 138 | addrWithPort = r.RemoteAddr + ":80" // assuming a default port 139 | } 140 | 141 | remoteAddr, err := net.ResolveTCPAddr("tcp", addrWithPort) 142 | if err != nil { 143 | fmt.Printf("could not resolve TCP address for addr %s\n", r.RemoteAddr) 144 | log.Println(err) 145 | http.Error(w, utils.StatusMessage(fiber.StatusInternalServerError), fiber.StatusInternalServerError) 146 | return 147 | } 148 | 149 | // New fasthttp Ctx 150 | var fctx fasthttp.RequestCtx 151 | fctx.Init(req, remoteAddr, nil) 152 | 153 | // Pass RequestCtx to Fiber router 154 | f.app.Handler()(&fctx) 155 | 156 | // Set response headers 157 | fctx.Response.Header.VisitAll(func(k, v []byte) { 158 | w.Header().Add(utils.UnsafeString(k), utils.UnsafeString(v)) 159 | }) 160 | 161 | // Set response statuscode 162 | w.WriteHeader(fctx.Response.StatusCode()) 163 | 164 | // Set response body 165 | _, _ = w.Write(fctx.Response.Body()) 166 | } 167 | -------------------------------------------------------------------------------- /fiber/fiber_suite_test.go: -------------------------------------------------------------------------------- 1 | package fiberadapter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestFiber(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Fiber Suite") 13 | } 14 | -------------------------------------------------------------------------------- /fiber/fiberlambda_test.go: -------------------------------------------------------------------------------- 1 | package fiberadapter_test 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-lambda-go/events" 7 | "github.com/gofiber/fiber/v2" 8 | 9 | fiberadaptor "github.com/awslabs/aws-lambda-go-api-proxy/fiber" 10 | 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | var _ = Describe("FiberLambda tests", func() { 16 | Context("Simple ping request", func() { 17 | It("Proxies the event correctly", func() { 18 | app := fiber.New() 19 | app.Get("/ping", func(c *fiber.Ctx) error { 20 | return c.SendString("pong") 21 | }) 22 | 23 | adapter := fiberadaptor.New(app) 24 | 25 | req := events.APIGatewayProxyRequest{ 26 | Path: "/ping", 27 | HTTPMethod: "GET", 28 | } 29 | 30 | resp, err := adapter.ProxyWithContext(context.Background(), req) 31 | 32 | Expect(err).To(BeNil()) 33 | Expect(resp.StatusCode).To(Equal(200)) 34 | 35 | resp, err = adapter.Proxy(req) 36 | 37 | Expect(err).To(BeNil()) 38 | Expect(resp.StatusCode).To(Equal(200)) 39 | }) 40 | }) 41 | 42 | Context("RemoteAddr handling", func() { 43 | It("Properly parses the IP address", func() { 44 | app := fiber.New() 45 | app.Get("/ping", func(c *fiber.Ctx) error { 46 | // make sure the ip address is actually set properly 47 | Expect(c.Context().RemoteAddr().String()).To(Equal("8.8.8.8:80")) 48 | return c.SendString("pong") 49 | }) 50 | 51 | adapter := fiberadaptor.New(app) 52 | 53 | req := events.APIGatewayProxyRequest{ 54 | Path: "/ping", 55 | HTTPMethod: "GET", 56 | RequestContext: events.APIGatewayProxyRequestContext{ 57 | Identity: events.APIGatewayRequestIdentity{ 58 | SourceIP: "8.8.8.8", 59 | }, 60 | }, 61 | } 62 | 63 | resp, err := adapter.ProxyWithContext(context.Background(), req) 64 | 65 | Expect(err).To(BeNil()) 66 | Expect(resp.StatusCode).To(Equal(200)) 67 | 68 | resp, err = adapter.Proxy(req) 69 | 70 | Expect(err).To(BeNil()) 71 | Expect(resp.StatusCode).To(Equal(200)) 72 | }) 73 | }) 74 | 75 | Context("Request header", func() { 76 | It("Check pass canonical header to fiber", func() { 77 | app := fiber.New() 78 | app.Post("/canonical_header", func(c *fiber.Ctx) error { 79 | Expect(c.Get(fiber.HeaderHost)).To(Equal("localhost")) 80 | Expect(c.Get(fiber.HeaderContentType)).To(Equal(fiber.MIMEApplicationJSONCharsetUTF8)) 81 | Expect(c.Get(fiber.HeaderUserAgent)).To(Equal("fiber")) 82 | 83 | Expect(c.Cookies("a")).To(Equal("b")) 84 | Expect(c.Cookies("b")).To(Equal("c")) 85 | Expect(c.Cookies("c")).To(Equal("d")) 86 | 87 | Expect(c.Get(fiber.HeaderContentLength)).To(Equal("77")) 88 | Expect(c.Get(fiber.HeaderConnection)).To(Equal("Keep-Alive")) 89 | Expect(c.Get(fiber.HeaderKeepAlive)).To(Equal("timeout=5, max=1000")) 90 | 91 | return c.Status(fiber.StatusNoContent).Send(nil) 92 | }) 93 | 94 | adapter := fiberadaptor.New(app) 95 | 96 | req := events.APIGatewayProxyRequest{ 97 | Path: "/canonical_header", 98 | HTTPMethod: "POST", 99 | MultiValueHeaders: map[string][]string{ 100 | fiber.HeaderHost: {"localhost"}, 101 | fiber.HeaderContentType: {fiber.MIMEApplicationJSONCharsetUTF8}, 102 | fiber.HeaderUserAgent: {"fiber"}, 103 | 104 | "cookie": {"a=b", "b=c;c=d"}, 105 | 106 | fiber.HeaderContentLength: {"77"}, 107 | fiber.HeaderConnection: {"Keep-Alive"}, 108 | fiber.HeaderKeepAlive: {"timeout=5, max=1000"}, 109 | }, 110 | } 111 | 112 | resp, err := adapter.ProxyWithContext(context.Background(), req) 113 | 114 | Expect(err).To(BeNil()) 115 | Expect(resp.StatusCode).To(Equal(fiber.StatusNoContent)) 116 | Expect(resp.Body).To(Equal("")) 117 | }) 118 | 119 | It("Check pass non canonical header to fiber", func() { 120 | app := fiber.New() 121 | app.Post("/header", func(c *fiber.Ctx) error { 122 | Expect(c.Get(fiber.HeaderReferer)).To(Equal("https://github.com/gofiber/fiber")) 123 | Expect(c.Get(fiber.HeaderAuthorization)).To(Equal("Bearer drink_beer_not_coffee")) 124 | 125 | c.Context().Request.Header.VisitAll(func(key, value []byte) { 126 | if string(key) == "K1" { 127 | Expect(Expect(c.Get("K1")).To(Or(Equal("v1"), Equal("v2")))) 128 | } 129 | }) 130 | 131 | return c.Status(fiber.StatusNoContent).Send(nil) 132 | }) 133 | 134 | adapter := fiberadaptor.New(app) 135 | 136 | req := events.APIGatewayProxyRequest{ 137 | Path: "/header", 138 | HTTPMethod: "POST", 139 | MultiValueHeaders: map[string][]string{ 140 | fiber.HeaderReferer: {"https://github.com/gofiber/fiber"}, 141 | fiber.HeaderAuthorization: {"Bearer drink_beer_not_coffee"}, 142 | 143 | "k1": {"v1", "v2"}, 144 | }, 145 | } 146 | 147 | resp, err := adapter.ProxyWithContext(context.Background(), req) 148 | 149 | Expect(err).To(BeNil()) 150 | Expect(resp.StatusCode).To(Equal(fiber.StatusNoContent)) 151 | Expect(resp.Body).To(Equal("")) 152 | }) 153 | }) 154 | 155 | Context("Response header", func() { 156 | It("Check pass canonical header to fiber", func() { 157 | app := fiber.New() 158 | app.Post("/canonical_header", func(c *fiber.Ctx) error { 159 | c.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSONCharsetUTF8) 160 | c.Set(fiber.HeaderServer, "localhost") 161 | 162 | c.Cookie(&fiber.Cookie{ 163 | Name: "a", 164 | Value: "b", 165 | HTTPOnly: true, 166 | }) 167 | c.Cookie(&fiber.Cookie{ 168 | Name: "b", 169 | Value: "c", 170 | HTTPOnly: true, 171 | }) 172 | c.Cookie(&fiber.Cookie{ 173 | Name: "c", 174 | Value: "d", 175 | HTTPOnly: true, 176 | }) 177 | 178 | c.Set(fiber.HeaderContentLength, "77") 179 | c.Set(fiber.HeaderConnection, "keep-alive") 180 | 181 | return c.Status(fiber.StatusNoContent).Send(nil) 182 | }) 183 | 184 | adapter := fiberadaptor.New(app) 185 | 186 | req := events.APIGatewayProxyRequest{ 187 | Path: "/canonical_header", 188 | HTTPMethod: "POST", 189 | } 190 | 191 | resp, err := adapter.ProxyWithContext(context.Background(), req) 192 | 193 | Expect(err).To(BeNil()) 194 | Expect(resp.StatusCode).To(Equal(fiber.StatusNoContent)) 195 | // NOTI: core.NewProxyResponseWriter().GetProxyResponse() => Doesn't use `resp.Header` 196 | Expect(resp.MultiValueHeaders[fiber.HeaderContentType]).To(Equal([]string{fiber.MIMEApplicationJSONCharsetUTF8})) 197 | Expect(resp.MultiValueHeaders[fiber.HeaderServer]).To(Equal([]string{"localhost"})) 198 | Expect(resp.MultiValueHeaders[fiber.HeaderSetCookie]).To(Equal([]string{"a=b; path=/; HttpOnly; SameSite=Lax", "b=c; path=/; HttpOnly; SameSite=Lax", "c=d; path=/; HttpOnly; SameSite=Lax"})) 199 | Expect(resp.MultiValueHeaders[fiber.HeaderContentLength]).To(Equal([]string{"77"})) 200 | Expect(resp.MultiValueHeaders[fiber.HeaderConnection]).To(Equal([]string{"keep-alive"})) 201 | Expect(resp.Body).To(Equal("")) 202 | }) 203 | It("Check pass non canonical header to fiber", func() { 204 | app := fiber.New() 205 | app.Post("/header", func(c *fiber.Ctx) error { 206 | c.Links("http://api.example.com/users?page=2", "next", "http://api.example.com/users?page=5", "last") 207 | return c.Redirect("https://github.com/gofiber/fiber") 208 | }) 209 | 210 | adapter := fiberadaptor.New(app) 211 | 212 | req := events.APIGatewayProxyRequest{ 213 | Path: "/header", 214 | HTTPMethod: "POST", 215 | } 216 | 217 | resp, err := adapter.ProxyWithContext(context.Background(), req) 218 | 219 | Expect(err).To(BeNil()) 220 | Expect(resp.StatusCode).To(Equal(fiber.StatusFound)) 221 | Expect(resp.MultiValueHeaders[fiber.HeaderLocation]).To(Equal([]string{"https://github.com/gofiber/fiber"})) 222 | Expect(resp.MultiValueHeaders[fiber.HeaderLink]).To(Equal([]string{"; rel=\"next\",; rel=\"last\""})) 223 | Expect(resp.Body).To(Equal("")) 224 | }) 225 | }) 226 | 227 | Context("Next method", func() { 228 | It("Check missing values in request header", func() { 229 | app := fiber.New() 230 | app.Post("/next", func(c *fiber.Ctx) error { 231 | c.Next() 232 | Expect(c.Get(fiber.HeaderHost)).To(Equal("localhost")) 233 | Expect(c.Get(fiber.HeaderContentType)).To(Equal(fiber.MIMEApplicationJSONCharsetUTF8)) 234 | Expect(c.Get(fiber.HeaderUserAgent)).To(Equal("fiber")) 235 | 236 | Expect(c.Cookies("a")).To(Equal("b")) 237 | Expect(c.Cookies("b")).To(Equal("c")) 238 | Expect(c.Cookies("c")).To(Equal("d")) 239 | 240 | Expect(c.Get(fiber.HeaderContentLength)).To(Equal("77")) 241 | Expect(c.Get(fiber.HeaderConnection)).To(Equal("Keep-Alive")) 242 | Expect(c.Get(fiber.HeaderKeepAlive)).To(Equal("timeout=5, max=1000")) 243 | 244 | return c.Status(fiber.StatusNoContent).Send(nil) 245 | }) 246 | adapter := fiberadaptor.New(app) 247 | 248 | req := events.APIGatewayProxyRequest{ 249 | Path: "/next", 250 | HTTPMethod: "POST", 251 | MultiValueHeaders: map[string][]string{ 252 | fiber.HeaderHost: {"localhost"}, 253 | fiber.HeaderContentType: {fiber.MIMEApplicationJSONCharsetUTF8}, 254 | fiber.HeaderUserAgent: {"fiber"}, 255 | 256 | "cookie": {"a=b", "b=c;c=d"}, 257 | 258 | fiber.HeaderContentLength: {"77"}, 259 | fiber.HeaderConnection: {"Keep-Alive"}, 260 | fiber.HeaderKeepAlive: {"timeout=5, max=1000"}, 261 | }, 262 | } 263 | 264 | resp, err := adapter.ProxyWithContext(context.Background(), req) 265 | 266 | Expect(err).To(BeNil()) 267 | Expect(resp.StatusCode).To(Equal(fiber.StatusNoContent)) 268 | Expect(resp.Body).To(Equal("")) 269 | }) 270 | 271 | It("Check missing values in response header", func() { 272 | app := fiber.New() 273 | app.Post("/next", func(c *fiber.Ctx) error { 274 | c.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSONCharsetUTF8) 275 | c.Set(fiber.HeaderServer, "localhost") 276 | 277 | c.Cookie(&fiber.Cookie{ 278 | Name: "a", 279 | Value: "b", 280 | HTTPOnly: true, 281 | }) 282 | c.Cookie(&fiber.Cookie{ 283 | Name: "b", 284 | Value: "c", 285 | HTTPOnly: true, 286 | }) 287 | c.Cookie(&fiber.Cookie{ 288 | Name: "c", 289 | Value: "d", 290 | HTTPOnly: true, 291 | }) 292 | 293 | c.Set(fiber.HeaderContentLength, "77") 294 | c.Set(fiber.HeaderConnection, "keep-alive") 295 | 296 | c.Next() 297 | return c.Status(fiber.StatusNoContent).Send(nil) 298 | }) 299 | adapter := fiberadaptor.New(app) 300 | 301 | req := events.APIGatewayProxyRequest{ 302 | Path: "/next", 303 | HTTPMethod: "POST", 304 | } 305 | 306 | resp, err := adapter.ProxyWithContext(context.Background(), req) 307 | 308 | Expect(err).To(BeNil()) 309 | Expect(resp.StatusCode).To(Equal(fiber.StatusNoContent)) 310 | Expect(resp.MultiValueHeaders[fiber.HeaderContentType]).To(Equal([]string{fiber.MIMEApplicationJSONCharsetUTF8})) 311 | Expect(resp.MultiValueHeaders[fiber.HeaderServer]).To(Equal([]string{"localhost"})) 312 | Expect(resp.MultiValueHeaders[fiber.HeaderSetCookie]).To(Equal([]string{"a=b; path=/; HttpOnly; SameSite=Lax", "b=c; path=/; HttpOnly; SameSite=Lax", "c=d; path=/; HttpOnly; SameSite=Lax"})) 313 | Expect(resp.MultiValueHeaders[fiber.HeaderContentLength]).To(Equal([]string{"77"})) 314 | Expect(resp.MultiValueHeaders[fiber.HeaderConnection]).To(Equal([]string{"keep-alive"})) 315 | Expect(resp.Body).To(Equal("")) 316 | }) 317 | }) 318 | }) 319 | -------------------------------------------------------------------------------- /gin/adapter.go: -------------------------------------------------------------------------------- 1 | // Package ginadapter adds Gin support for the aws-severless-go-api library. 2 | // Uses the core package behind the scenes and exposes the New and NewV2 methods to 3 | // get a new instance and Proxy method to send request to the Gin engine. 4 | package ginadapter 5 | 6 | import ( 7 | "context" 8 | "net/http" 9 | 10 | "github.com/aws/aws-lambda-go/events" 11 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | // GinLambda makes it easy to send API Gateway proxy events to a Gin 16 | // Engine. The library transforms the proxy event into an HTTP request and then 17 | // creates a proxy response object from the http.ResponseWriter 18 | type GinLambda struct { 19 | core.RequestAccessor 20 | 21 | ginEngine *gin.Engine 22 | } 23 | 24 | // New creates a new instance of the GinLambda object. 25 | // Receives an initialized *gin.Engine object - normally created with gin.Default(). 26 | // It returns the initialized instance of the GinLambda object. 27 | func New(gin *gin.Engine) *GinLambda { 28 | return &GinLambda{ginEngine: gin} 29 | } 30 | 31 | // Proxy receives an API Gateway proxy event, transforms it into an http.Request 32 | // object, and sends it to the gin.Engine for routing. 33 | // It returns a proxy response object generated from the http.ResponseWriter. 34 | func (g *GinLambda) Proxy(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 35 | ginRequest, err := g.ProxyEventToHTTPRequest(req) 36 | return g.proxyInternal(ginRequest, err) 37 | } 38 | 39 | // ProxyWithContext receives context and an API Gateway proxy event, 40 | // transforms them into an http.Request object, and sends it to the gin.Engine for routing. 41 | // It returns a proxy response object generated from the http.ResponseWriter. 42 | func (g *GinLambda) ProxyWithContext(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 43 | ginRequest, err := g.EventToRequestWithContext(ctx, req) 44 | return g.proxyInternal(ginRequest, err) 45 | } 46 | 47 | func (g *GinLambda) proxyInternal(req *http.Request, err error) (events.APIGatewayProxyResponse, error) { 48 | 49 | if err != nil { 50 | return core.GatewayTimeout(), core.NewLoggedError("Could not convert proxy event to request: %v", err) 51 | } 52 | 53 | respWriter := core.NewProxyResponseWriter() 54 | g.ginEngine.ServeHTTP(http.ResponseWriter(respWriter), req) 55 | 56 | proxyResponse, err := respWriter.GetProxyResponse() 57 | if err != nil { 58 | return core.GatewayTimeout(), core.NewLoggedError("Error while generating proxy response: %v", err) 59 | } 60 | 61 | return proxyResponse, nil 62 | } 63 | -------------------------------------------------------------------------------- /gin/adapterALB.go: -------------------------------------------------------------------------------- 1 | // Package ginadapter adds Gin support for the aws-severless-go-api library. 2 | // Uses the core package behind the scenes and exposes the New and NewV2 and ALB methods to 3 | // get a new instance and Proxy method to send request to the Gin engine. 4 | package ginadapter 5 | 6 | import ( 7 | "context" 8 | "net/http" 9 | 10 | "github.com/aws/aws-lambda-go/events" 11 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | // GinLambdaALB makes it easy to send ALB proxy events to a Gin 16 | // Engine. The library transforms the proxy event into an HTTP request and then 17 | // creates a proxy response object from the http.ResponseWriter 18 | type GinLambdaALB struct { 19 | core.RequestAccessorALB 20 | 21 | ginEngine *gin.Engine 22 | } 23 | 24 | // New creates a new instance of the GinLambdaALB object. 25 | // Receives an initialized *gin.Engine object - normally created with gin.Default(). 26 | // It returns the initialized instance of the GinLambdaALB object. 27 | func NewALB(gin *gin.Engine) *GinLambdaALB { 28 | return &GinLambdaALB{ginEngine: gin} 29 | } 30 | 31 | // Proxy receives an ALB proxy event, transforms it into an http.Request 32 | // object, and sends it to the gin.Engine for routing. 33 | // It returns a proxy response object generated from the http.ResponseWriter. 34 | func (g *GinLambdaALB) Proxy(req events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) { 35 | ginRequest, err := g.ProxyEventToHTTPRequest(req) 36 | return g.proxyInternal(ginRequest, err) 37 | } 38 | 39 | // ProxyWithContext receives context and an ALB proxy event, 40 | // transforms them into an http.Request object, and sends it to the gin.Engine for routing. 41 | // It returns a proxy response object generated from the http.ResponseWriter. 42 | func (g *GinLambdaALB) ProxyWithContext(ctx context.Context, req events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) { 43 | ginRequest, err := g.EventToRequestWithContext(ctx, req) 44 | return g.proxyInternal(ginRequest, err) 45 | } 46 | 47 | func (g *GinLambdaALB) proxyInternal(req *http.Request, err error) (events.ALBTargetGroupResponse, error) { 48 | 49 | if err != nil { 50 | return core.GatewayTimeoutALB(), core.NewLoggedError("Could not convert proxy event to request: %v", err) 51 | } 52 | 53 | respWriter := core.NewProxyResponseWriterALB() 54 | g.ginEngine.ServeHTTP(http.ResponseWriter(respWriter), req) 55 | 56 | proxyResponse, err := respWriter.GetProxyResponse() 57 | if err != nil { 58 | return core.GatewayTimeoutALB(), core.NewLoggedError("Error while generating proxy response: %v", err) 59 | } 60 | 61 | return proxyResponse, nil 62 | } 63 | -------------------------------------------------------------------------------- /gin/adapterv2.go: -------------------------------------------------------------------------------- 1 | package ginadapter 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/aws/aws-lambda-go/events" 8 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // GinLambdaV2 makes it easy to send API Gateway proxy V2 events to a Gin 13 | // Engine. The library transforms the proxy event into an HTTP request and then 14 | // creates a proxy response object from the http.ResponseWriter 15 | type GinLambdaV2 struct { 16 | core.RequestAccessorV2 17 | 18 | ginEngine *gin.Engine 19 | } 20 | 21 | // NewV2 creates a new instance of the GinLambdaV2 object. 22 | // Receives an initialized *gin.Engine object - normally created with gin.Default(). 23 | // It returns the initialized instance of the GinLambdaV2 object. 24 | func NewV2(gin *gin.Engine) *GinLambdaV2 { 25 | return &GinLambdaV2{ginEngine: gin} 26 | } 27 | 28 | // Proxy receives an API Gateway proxy V2 event, transforms it into an http.Request 29 | // object, and sends it to the gin.Engine for routing. 30 | // It returns an http response object generated from the http.ResponseWriter. 31 | func (g *GinLambdaV2) Proxy(req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 32 | ginRequest, err := g.ProxyEventToHTTPRequest(req) 33 | return g.proxyInternal(ginRequest, err) 34 | } 35 | 36 | // ProxyWithContext receives context and an API Gateway proxy V2 event, 37 | // transforms them into an http.Request object, and sends it to the gin.Engine for routing. 38 | // It returns an http response object generated from the http.ResponseWriter. 39 | func (g *GinLambdaV2) ProxyWithContext(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 40 | ginRequest, err := g.EventToRequestWithContext(ctx, req) 41 | return g.proxyInternal(ginRequest, err) 42 | } 43 | 44 | func (g *GinLambdaV2) proxyInternal(req *http.Request, err error) (events.APIGatewayV2HTTPResponse, error) { 45 | 46 | if err != nil { 47 | return core.GatewayTimeoutV2(), core.NewLoggedError("Could not convert proxy event to request: %v", err) 48 | } 49 | 50 | respWriter := core.NewProxyResponseWriterV2() 51 | g.ginEngine.ServeHTTP(http.ResponseWriter(respWriter), req) 52 | 53 | proxyResponse, err := respWriter.GetProxyResponse() 54 | if err != nil { 55 | return core.GatewayTimeoutV2(), core.NewLoggedError("Error while generating proxy response: %v", err) 56 | } 57 | 58 | return proxyResponse, nil 59 | } 60 | -------------------------------------------------------------------------------- /gin/gin_suite_test.go: -------------------------------------------------------------------------------- 1 | package ginadapter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestGin(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Gin Suite") 13 | } 14 | -------------------------------------------------------------------------------- /gin/ginlambda_test.go: -------------------------------------------------------------------------------- 1 | package ginadapter_test 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/aws/aws-lambda-go/events" 8 | ginadapter "github.com/awslabs/aws-lambda-go-api-proxy/gin" 9 | "github.com/gin-gonic/gin" 10 | 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | var _ = Describe("GinLambda tests", func() { 16 | Context("Simple ping request", func() { 17 | It("Proxies the event correctly", func() { 18 | log.Println("Starting test") 19 | r := gin.Default() 20 | r.GET("/ping", func(c *gin.Context) { 21 | log.Println("Handler!!") 22 | c.JSON(200, gin.H{ 23 | "message": "pong", 24 | }) 25 | }) 26 | 27 | adapter := ginadapter.New(r) 28 | 29 | req := events.APIGatewayProxyRequest{ 30 | Path: "/ping", 31 | HTTPMethod: "GET", 32 | } 33 | 34 | resp, err := adapter.ProxyWithContext(context.Background(), req) 35 | 36 | Expect(err).To(BeNil()) 37 | Expect(resp.StatusCode).To(Equal(200)) 38 | 39 | resp, err = adapter.Proxy(req) 40 | 41 | Expect(err).To(BeNil()) 42 | Expect(resp.StatusCode).To(Equal(200)) 43 | }) 44 | }) 45 | }) 46 | 47 | var _ = Describe("GinLambdaV2 tests", func() { 48 | Context("Simple ping request", func() { 49 | It("Proxies the event correctly", func() { 50 | log.Println("Starting test") 51 | r := gin.Default() 52 | r.GET("/ping", func(c *gin.Context) { 53 | log.Println("Handler!!") 54 | c.JSON(200, gin.H{ 55 | "message": "pong", 56 | }) 57 | }) 58 | 59 | adapter := ginadapter.NewV2(r) 60 | 61 | req := events.APIGatewayV2HTTPRequest{ 62 | RequestContext: events.APIGatewayV2HTTPRequestContext{ 63 | HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{ 64 | Method: "GET", 65 | Path: "/ping", 66 | }, 67 | }, 68 | } 69 | 70 | resp, err := adapter.ProxyWithContext(context.Background(), req) 71 | 72 | Expect(err).To(BeNil()) 73 | Expect(resp.StatusCode).To(Equal(200)) 74 | 75 | resp, err = adapter.Proxy(req) 76 | 77 | Expect(err).To(BeNil()) 78 | Expect(resp.StatusCode).To(Equal(200)) 79 | }) 80 | }) 81 | }) 82 | 83 | var _ = Describe("GinLambdaALB tests", func() { 84 | Context("Simple ping request", func() { 85 | It("Proxies the event correctly", func() { 86 | log.Println("Starting test") 87 | r := gin.Default() 88 | r.GET("/ping", func(c *gin.Context) { 89 | log.Println("Handler!!") 90 | c.JSON(200, gin.H{ 91 | "message": "pong", 92 | }) 93 | }) 94 | 95 | adapter := ginadapter.NewALB(r) 96 | 97 | req := events.ALBTargetGroupRequest{ 98 | HTTPMethod: "GET", 99 | Path: "/ping", 100 | RequestContext: events.ALBTargetGroupRequestContext{ 101 | ELB: events.ELBContext{TargetGroupArn: " ad"}, 102 | }} 103 | 104 | resp, err := adapter.Proxy(req) 105 | 106 | Expect(err).To(BeNil()) 107 | Expect(resp.StatusCode).To(Equal(200)) 108 | 109 | resp, err = adapter.Proxy(req) 110 | 111 | Expect(err).To(BeNil()) 112 | Expect(resp.StatusCode).To(Equal(200)) 113 | }) 114 | }) 115 | }) 116 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/awslabs/aws-lambda-go-api-proxy 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.6 6 | 7 | require ( 8 | github.com/aws/aws-lambda-go v1.41.0 9 | github.com/gin-gonic/gin v1.9.1 10 | github.com/go-chi/chi/v5 v5.0.8 11 | github.com/gofiber/fiber/v2 v2.52.5 12 | github.com/gorilla/mux v1.8.0 13 | github.com/kataras/iris/v12 v12.2.10 14 | github.com/labstack/echo/v4 v4.10.2 15 | github.com/onsi/ginkgo v1.16.5 16 | github.com/onsi/gomega v1.27.7 17 | github.com/urfave/negroni v1.0.0 18 | github.com/valyala/fasthttp v1.51.0 19 | ) 20 | 21 | require ( 22 | github.com/BurntSushi/toml v1.3.2 // indirect 23 | github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect 24 | github.com/CloudyKit/jet/v6 v6.2.0 // indirect 25 | github.com/Joker/jade v1.1.3 // indirect 26 | github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect 27 | github.com/andybalholm/brotli v1.1.0 // indirect 28 | github.com/aymerick/douceur v0.2.0 // indirect 29 | github.com/bytedance/sonic v1.9.1 // indirect 30 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 31 | github.com/fatih/structs v1.1.0 // indirect 32 | github.com/flosch/pongo2/v4 v4.0.2 // indirect 33 | github.com/fsnotify/fsnotify v1.7.0 // indirect 34 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 35 | github.com/gin-contrib/sse v0.1.0 // indirect 36 | github.com/go-playground/locales v0.14.1 // indirect 37 | github.com/go-playground/universal-translator v0.18.1 // indirect 38 | github.com/go-playground/validator/v10 v10.14.0 // indirect 39 | github.com/goccy/go-json v0.10.2 // indirect 40 | github.com/golang/snappy v0.0.4 // indirect 41 | github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 // indirect 42 | github.com/google/go-cmp v0.5.9 // indirect 43 | github.com/google/uuid v1.5.0 // indirect 44 | github.com/gorilla/css v1.0.0 // indirect 45 | github.com/iris-contrib/schema v0.0.6 // indirect 46 | github.com/josharian/intern v1.0.0 // indirect 47 | github.com/json-iterator/go v1.1.12 // indirect 48 | github.com/kataras/blocks v0.0.8 // indirect 49 | github.com/kataras/golog v0.1.11 // indirect 50 | github.com/kataras/pio v0.0.13 // indirect 51 | github.com/kataras/sitemap v0.0.6 // indirect 52 | github.com/kataras/tunnel v0.0.4 // indirect 53 | github.com/klauspost/compress v1.17.4 // indirect 54 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 55 | github.com/labstack/gommon v0.4.0 // indirect 56 | github.com/leodido/go-urn v1.2.4 // indirect 57 | github.com/mailgun/raymond/v2 v2.0.48 // indirect 58 | github.com/mailru/easyjson v0.7.7 // indirect 59 | github.com/mattn/go-colorable v0.1.13 // indirect 60 | github.com/mattn/go-isatty v0.0.20 // indirect 61 | github.com/mattn/go-runewidth v0.0.15 // indirect 62 | github.com/microcosm-cc/bluemonday v1.0.26 // indirect 63 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 64 | github.com/modern-go/reflect2 v1.0.2 // indirect 65 | github.com/nxadm/tail v1.4.11 // indirect 66 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 67 | github.com/rivo/uniseg v0.2.0 // indirect 68 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 69 | github.com/schollz/closestmatch v2.1.0+incompatible // indirect 70 | github.com/sirupsen/logrus v1.8.1 // indirect 71 | github.com/tdewolff/minify/v2 v2.20.14 // indirect 72 | github.com/tdewolff/parse/v2 v2.7.8 // indirect 73 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 74 | github.com/ugorji/go/codec v1.2.11 // indirect 75 | github.com/valyala/bytebufferpool v1.0.0 // indirect 76 | github.com/valyala/fasttemplate v1.2.2 // indirect 77 | github.com/valyala/tcplisten v1.0.0 // indirect 78 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 79 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 80 | github.com/yosssi/ace v0.0.5 // indirect 81 | golang.org/x/arch v0.3.0 // indirect 82 | golang.org/x/crypto v0.18.0 // indirect 83 | golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect 84 | golang.org/x/net v0.20.0 // indirect 85 | golang.org/x/sys v0.16.0 // indirect 86 | golang.org/x/text v0.14.0 // indirect 87 | golang.org/x/time v0.5.0 // indirect 88 | google.golang.org/protobuf v1.33.0 // indirect 89 | gopkg.in/ini.v1 v1.67.0 // indirect 90 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 91 | gopkg.in/yaml.v3 v3.0.1 // indirect 92 | ) 93 | -------------------------------------------------------------------------------- /gorillamux/adapter.go: -------------------------------------------------------------------------------- 1 | package gorillamux 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 9 | "github.com/gorilla/mux" 10 | ) 11 | 12 | type GorillaMuxAdapter struct { 13 | RequestAccessor core.RequestAccessor 14 | RequestAccessorV2 core.RequestAccessorV2 15 | router *mux.Router 16 | } 17 | 18 | func New(router *mux.Router) *GorillaMuxAdapter { 19 | return &GorillaMuxAdapter{ 20 | router: router, 21 | } 22 | } 23 | 24 | // Proxy receives an API Gateway proxy event or API Gateway V2 event, transforms it into an http.Request 25 | // object, and sends it to the mux.Router for routing. 26 | // It returns a proxy response object generated from the http.ResponseWriter. 27 | func (h *GorillaMuxAdapter) Proxy(event core.SwitchableAPIGatewayRequest) (*core.SwitchableAPIGatewayResponse, error) { 28 | if event.Version1() != nil { 29 | req, err := h.RequestAccessor.ProxyEventToHTTPRequest(*event.Version1()) 30 | return h.proxyInternal(req, err) 31 | } else if event.Version2() != nil { 32 | req, err := h.RequestAccessorV2.ProxyEventToHTTPRequest(*event.Version2()) 33 | return h.proxyInternalV2(req, err) 34 | } else { 35 | return &core.SwitchableAPIGatewayResponse{}, core.NewLoggedError("Could not convert proxy event to request: %v", errors.New("Unable to determine version ")) 36 | } 37 | } 38 | 39 | // ProxyWithContext receives context and an API Gateway proxy event or API Gateway V2 event, 40 | // transforms them into an http.Request object, and sends it to the mux.Router for routing. 41 | // It returns a proxy response object generated from the http.ResponseWriter. 42 | func (h *GorillaMuxAdapter) ProxyWithContext(ctx context.Context, event core.SwitchableAPIGatewayRequest) (*core.SwitchableAPIGatewayResponse, error) { 43 | if event.Version1() != nil { 44 | req, err := h.RequestAccessor.EventToRequestWithContext(ctx, *event.Version1()) 45 | return h.proxyInternal(req, err) 46 | } else if event.Version2() != nil { 47 | req, err := h.RequestAccessorV2.EventToRequestWithContext(ctx, *event.Version2()) 48 | return h.proxyInternalV2(req, err) 49 | } else { 50 | return &core.SwitchableAPIGatewayResponse{}, core.NewLoggedError("Could not convert proxy event to request: %v", errors.New("Unable to determine version ")) 51 | } 52 | } 53 | 54 | func (h *GorillaMuxAdapter) proxyInternal(req *http.Request, err error) (*core.SwitchableAPIGatewayResponse, error) { 55 | if err != nil { 56 | timeout := core.GatewayTimeout() 57 | return core.NewSwitchableAPIGatewayResponseV1(&timeout), core.NewLoggedError("Could not convert proxy event to request: %v", err) 58 | } 59 | 60 | w := core.NewProxyResponseWriter() 61 | h.router.ServeHTTP(http.ResponseWriter(w), req) 62 | 63 | resp, err := w.GetProxyResponse() 64 | if err != nil { 65 | timeout := core.GatewayTimeout() 66 | return core.NewSwitchableAPIGatewayResponseV1(&timeout), core.NewLoggedError("Error while generating proxy response: %v", err) 67 | } 68 | 69 | return core.NewSwitchableAPIGatewayResponseV1(&resp), nil 70 | } 71 | 72 | func (h *GorillaMuxAdapter) proxyInternalV2(req *http.Request, err error) (*core.SwitchableAPIGatewayResponse, error) { 73 | if err != nil { 74 | timeout := core.GatewayTimeoutV2() 75 | return core.NewSwitchableAPIGatewayResponseV2(&timeout), core.NewLoggedError("Could not convert proxy event to request: %v", err) 76 | } 77 | 78 | w := core.NewProxyResponseWriterV2() 79 | h.router.ServeHTTP(http.ResponseWriter(w), req) 80 | 81 | resp, err := w.GetProxyResponse() 82 | if err != nil { 83 | timeout := core.GatewayTimeoutV2() 84 | return core.NewSwitchableAPIGatewayResponseV2(&timeout), core.NewLoggedError("Error while generating proxy response: %v", err) 85 | } 86 | 87 | return core.NewSwitchableAPIGatewayResponseV2(&resp), nil 88 | } 89 | -------------------------------------------------------------------------------- /gorillamux/adapterALB.go: -------------------------------------------------------------------------------- 1 | package gorillamux 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/aws/aws-lambda-go/events" 8 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 9 | "github.com/gorilla/mux" 10 | ) 11 | 12 | type GorillaMuxAdapterALB struct { 13 | core.RequestAccessorALB 14 | router *mux.Router 15 | } 16 | 17 | func NewALB(router *mux.Router) *GorillaMuxAdapterALB { 18 | return &GorillaMuxAdapterALB{ 19 | router: router, 20 | } 21 | } 22 | 23 | // Proxy receives an API Gateway proxy event, transforms it into an http.Request 24 | // object, and sends it to the mux.Router for routing. 25 | // It returns a proxy response object generated from the http.ResponseWriter. 26 | func (h *GorillaMuxAdapterALB) Proxy(event events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) { 27 | req, err := h.ProxyEventToHTTPRequest(event) 28 | return h.proxyInternal(req, err) 29 | } 30 | 31 | // ProxyWithContext receives context and an API Gateway proxy event, 32 | // transforms them into an http.Request object, and sends it to the mux.Router for routing. 33 | // It returns a proxy response object generated from the http.ResponseWriter. 34 | func (h *GorillaMuxAdapterALB) ProxyWithContext(ctx context.Context, event events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) { 35 | req, err := h.EventToRequestWithContext(ctx, event) 36 | return h.proxyInternal(req, err) 37 | } 38 | 39 | func (h *GorillaMuxAdapterALB) proxyInternal(req *http.Request, err error) (events.ALBTargetGroupResponse, error) { 40 | if err != nil { 41 | return core.GatewayTimeoutALB(), core.NewLoggedError("Could not convert proxy event to request: %v", err) 42 | } 43 | 44 | w := core.NewProxyResponseWriterALB() 45 | h.router.ServeHTTP(http.ResponseWriter(w), req) 46 | 47 | resp, err := w.GetProxyResponse() 48 | if err != nil { 49 | return core.GatewayTimeoutALB(), core.NewLoggedError("Error while generating proxy response: %v", err) 50 | } 51 | 52 | return resp, nil 53 | } 54 | -------------------------------------------------------------------------------- /gorillamux/adapterALB_test.go: -------------------------------------------------------------------------------- 1 | package gorillamux_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/aws/aws-lambda-go/events" 9 | "github.com/awslabs/aws-lambda-go-api-proxy/gorillamux" 10 | "github.com/gorilla/mux" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("GorillaMuxAdapterALB tests", func() { 17 | Context("Simple ping request", func() { 18 | It("Proxies the event correctly", func() { 19 | homeHandler := func(w http.ResponseWriter, req *http.Request) { 20 | w.Header().Add("unfortunately-required-header", "") 21 | fmt.Fprintf(w, "Home Page") 22 | } 23 | 24 | productsHandler := func(w http.ResponseWriter, req *http.Request) { 25 | w.Header().Add("unfortunately-required-header", "") 26 | fmt.Fprintf(w, "Products Page") 27 | } 28 | 29 | r := mux.NewRouter() 30 | r.HandleFunc("/", homeHandler) 31 | r.HandleFunc("/products", productsHandler) 32 | 33 | adapter := gorillamux.NewALB(r) 34 | 35 | homePageReq := events.ALBTargetGroupRequest{ 36 | HTTPMethod: http.MethodGet, 37 | Path: "/", 38 | RequestContext: events.ALBTargetGroupRequestContext{ 39 | ELB: events.ELBContext{TargetGroupArn: " ad"}, 40 | }} 41 | 42 | homePageResp, homePageReqErr := adapter.ProxyWithContext(context.Background(), homePageReq) 43 | 44 | Expect(homePageReqErr).To(BeNil()) 45 | Expect(homePageResp.StatusCode).To(Equal(200)) 46 | Expect(homePageResp.Body).To(Equal("Home Page")) 47 | 48 | productsPageReq := events.ALBTargetGroupRequest{ 49 | HTTPMethod: http.MethodGet, 50 | Path: "/products", 51 | RequestContext: events.ALBTargetGroupRequestContext{ 52 | ELB: events.ELBContext{TargetGroupArn: " ad"}, 53 | }} 54 | 55 | productsPageResp, productsPageReqErr := adapter.Proxy(productsPageReq) 56 | 57 | Expect(productsPageReqErr).To(BeNil()) 58 | Expect(productsPageResp.StatusCode).To(Equal(200)) 59 | Expect(productsPageResp.Body).To(Equal("Products Page")) 60 | }) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /gorillamux/adapter_test.go: -------------------------------------------------------------------------------- 1 | package gorillamux_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 7 | "net/http" 8 | 9 | "github.com/aws/aws-lambda-go/events" 10 | "github.com/awslabs/aws-lambda-go-api-proxy/gorillamux" 11 | "github.com/gorilla/mux" 12 | 13 | . "github.com/onsi/ginkgo" 14 | . "github.com/onsi/gomega" 15 | ) 16 | 17 | var _ = Describe("GorillaMuxAdapter tests", func() { 18 | Context("Simple ping request with v1", func() { 19 | It("Proxies the event correctly", func() { 20 | homeHandler := func(w http.ResponseWriter, req *http.Request) { 21 | w.Header().Add("unfortunately-required-header", "") 22 | fmt.Fprintf(w, "Home Page") 23 | } 24 | 25 | productsHandler := func(w http.ResponseWriter, req *http.Request) { 26 | w.Header().Add("unfortunately-required-header", "") 27 | fmt.Fprintf(w, "Products Page") 28 | } 29 | 30 | r := mux.NewRouter() 31 | r.HandleFunc("/", homeHandler) 32 | r.HandleFunc("/products", productsHandler) 33 | 34 | adapter := gorillamux.New(r) 35 | 36 | homePageReq := events.APIGatewayProxyRequest{ 37 | Path: "/", 38 | HTTPMethod: "GET", 39 | } 40 | 41 | homePageResp, homePageReqErr := adapter.ProxyWithContext(context.Background(), *core.NewSwitchableAPIGatewayRequestV1(&homePageReq)) 42 | 43 | Expect(homePageReqErr).To(BeNil()) 44 | Expect(homePageResp.Version1().StatusCode).To(Equal(200)) 45 | Expect(homePageResp.Version1().Body).To(Equal("Home Page")) 46 | 47 | productsPageReq := events.APIGatewayProxyRequest{ 48 | Path: "/products", 49 | HTTPMethod: "GET", 50 | } 51 | 52 | productsPageResp, productsPageReqErr := adapter.Proxy(*core.NewSwitchableAPIGatewayRequestV1(&productsPageReq)) 53 | 54 | Expect(productsPageReqErr).To(BeNil()) 55 | Expect(productsPageResp.Version1().StatusCode).To(Equal(200)) 56 | Expect(productsPageResp.Version1().Body).To(Equal("Products Page")) 57 | }) 58 | }) 59 | 60 | Context("Simple ping request with v2", func() { 61 | It("Proxies the event correctly", func() { 62 | homeHandler := func(w http.ResponseWriter, req *http.Request) { 63 | w.Header().Add("unfortunately-required-header", "") 64 | fmt.Fprintf(w, "Home Page") 65 | } 66 | 67 | productsHandler := func(w http.ResponseWriter, req *http.Request) { 68 | w.Header().Add("unfortunately-required-header", "") 69 | fmt.Fprintf(w, "Products Page") 70 | } 71 | 72 | r := mux.NewRouter() 73 | r.HandleFunc("/", homeHandler) 74 | r.HandleFunc("/products", productsHandler) 75 | 76 | adapter := gorillamux.New(r) 77 | 78 | homePageReq := getProxyRequestV2("/", "GET") 79 | 80 | homePageResp, homePageReqErr := adapter.ProxyWithContext(context.Background(), *core.NewSwitchableAPIGatewayRequestV2(&homePageReq)) 81 | 82 | Expect(homePageReqErr).To(BeNil()) 83 | Expect(homePageResp.Version2().StatusCode).To(Equal(200)) 84 | Expect(homePageResp.Version2().Body).To(Equal("Home Page")) 85 | 86 | productsPageReq := getProxyRequestV2("/products", "GET") 87 | 88 | productsPageResp, productsPageReqErr := adapter.Proxy(*core.NewSwitchableAPIGatewayRequestV2(&productsPageReq)) 89 | 90 | Expect(productsPageReqErr).To(BeNil()) 91 | Expect(productsPageResp.Version2().StatusCode).To(Equal(200)) 92 | Expect(productsPageResp.Version2().Body).To(Equal("Products Page")) 93 | }) 94 | }) 95 | }) 96 | 97 | func getProxyRequestV2(path string, method string) events.APIGatewayV2HTTPRequest { 98 | return events.APIGatewayV2HTTPRequest{ 99 | RequestContext: events.APIGatewayV2HTTPRequestContext{ 100 | HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{ 101 | Path: path, 102 | Method: method, 103 | }, 104 | }, 105 | RawPath: path, 106 | } 107 | } 108 | 109 | -------------------------------------------------------------------------------- /gorillamux/adapterv2.go: -------------------------------------------------------------------------------- 1 | package gorillamux 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/aws/aws-lambda-go/events" 8 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 9 | "github.com/gorilla/mux" 10 | ) 11 | 12 | type GorillaMuxAdapterV2 struct { 13 | core.RequestAccessorV2 14 | router *mux.Router 15 | } 16 | 17 | func NewV2(router *mux.Router) *GorillaMuxAdapterV2 { 18 | return &GorillaMuxAdapterV2{ 19 | router: router, 20 | } 21 | } 22 | 23 | // Proxy receives an API Gateway proxy event, transforms it into an http.Request 24 | // object, and sends it to the mux.Router for routing. 25 | // It returns a proxy response object generated from the http.ResponseWriter. 26 | func (h *GorillaMuxAdapterV2) Proxy(event events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 27 | req, err := h.ProxyEventToHTTPRequest(event) 28 | return h.proxyInternal(req, err) 29 | } 30 | 31 | // ProxyWithContext receives context and an API Gateway proxy event, 32 | // transforms them into an http.Request object, and sends it to the mux.Router for routing. 33 | // It returns a proxy response object generated from the http.ResponseWriter. 34 | func (h *GorillaMuxAdapterV2) ProxyWithContext(ctx context.Context, event events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 35 | req, err := h.EventToRequestWithContext(ctx, event) 36 | return h.proxyInternal(req, err) 37 | } 38 | 39 | func (h *GorillaMuxAdapterV2) proxyInternal(req *http.Request, err error) (events.APIGatewayV2HTTPResponse, error) { 40 | if err != nil { 41 | return core.GatewayTimeoutV2(), core.NewLoggedError("Could not convert proxy event to request: %v", err) 42 | } 43 | 44 | w := core.NewProxyResponseWriterV2() 45 | h.router.ServeHTTP(http.ResponseWriter(w), req) 46 | 47 | resp, err := w.GetProxyResponse() 48 | if err != nil { 49 | return core.GatewayTimeoutV2(), core.NewLoggedError("Error while generating proxy response: %v", err) 50 | } 51 | 52 | return resp, nil 53 | } 54 | -------------------------------------------------------------------------------- /gorillamux/adapterv2_test.go: -------------------------------------------------------------------------------- 1 | package gorillamux_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/aws/aws-lambda-go/events" 9 | "github.com/awslabs/aws-lambda-go-api-proxy/gorillamux" 10 | "github.com/gorilla/mux" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("GorillaMuxAdapterV2 tests", func() { 17 | Context("Simple ping request", func() { 18 | It("Proxies the event correctly", func() { 19 | homeHandler := func(w http.ResponseWriter, req *http.Request) { 20 | w.Header().Add("unfortunately-required-header", "") 21 | fmt.Fprintf(w, "Home Page") 22 | } 23 | 24 | productsHandler := func(w http.ResponseWriter, req *http.Request) { 25 | w.Header().Add("unfortunately-required-header", "") 26 | fmt.Fprintf(w, "Products Page") 27 | } 28 | 29 | r := mux.NewRouter() 30 | r.HandleFunc("/", homeHandler) 31 | r.HandleFunc("/products", productsHandler) 32 | 33 | adapter := gorillamux.NewV2(r) 34 | 35 | homePageReq := events.APIGatewayV2HTTPRequest{ 36 | RequestContext: events.APIGatewayV2HTTPRequestContext{ 37 | HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{ 38 | Method: http.MethodGet, 39 | Path: "/", 40 | }, 41 | }, 42 | } 43 | 44 | homePageResp, homePageReqErr := adapter.ProxyWithContext(context.Background(), homePageReq) 45 | 46 | Expect(homePageReqErr).To(BeNil()) 47 | Expect(homePageResp.StatusCode).To(Equal(200)) 48 | Expect(homePageResp.Body).To(Equal("Home Page")) 49 | 50 | productsPageReq := events.APIGatewayV2HTTPRequest{ 51 | RequestContext: events.APIGatewayV2HTTPRequestContext{ 52 | HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{ 53 | Method: http.MethodGet, 54 | Path: "/products", 55 | }, 56 | }, 57 | } 58 | 59 | productsPageResp, productsPageReqErr := adapter.Proxy(productsPageReq) 60 | 61 | Expect(productsPageReqErr).To(BeNil()) 62 | Expect(productsPageResp.StatusCode).To(Equal(200)) 63 | Expect(productsPageResp.Body).To(Equal("Products Page")) 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /gorillamux/gorilla_suite_test.go: -------------------------------------------------------------------------------- 1 | package gorillamux_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestGorillaMux(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Gorilla Mux Suite") 13 | } 14 | -------------------------------------------------------------------------------- /handlerfunc/adapter.go: -------------------------------------------------------------------------------- 1 | package handlerfunc 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" 7 | ) 8 | 9 | type HandlerFuncAdapter = httpadapter.HandlerAdapter 10 | 11 | func New(handlerFunc http.HandlerFunc) *HandlerFuncAdapter { 12 | return httpadapter.New(handlerFunc) 13 | } 14 | -------------------------------------------------------------------------------- /handlerfunc/adapterALB.go: -------------------------------------------------------------------------------- 1 | package handlerfunc 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" 7 | ) 8 | 9 | type HandlerFuncAdapterALB = httpadapter.HandlerAdapterALB 10 | 11 | func NewALB(handlerFunc http.HandlerFunc) *HandlerFuncAdapterALB { 12 | return httpadapter.NewALB(handlerFunc) 13 | } 14 | -------------------------------------------------------------------------------- /handlerfunc/adapterALB_test.go: -------------------------------------------------------------------------------- 1 | package handlerfunc_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/aws/aws-lambda-go/events" 10 | "github.com/awslabs/aws-lambda-go-api-proxy/handlerfunc" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("HandlerFuncAdapter ALB tests", func() { 17 | Context("Simple ping request", func() { 18 | It("Proxies the event correctly", func() { 19 | log.Println("Starting test") 20 | 21 | handler := func(w http.ResponseWriter, req *http.Request) { 22 | w.Header().Add("unfortunately-required-header", "") 23 | fmt.Fprintf(w, "Go Lambda!!") 24 | } 25 | 26 | adapter := handlerfunc.NewALB(handler) 27 | 28 | req := events.ALBTargetGroupRequest{ 29 | HTTPMethod: http.MethodGet, 30 | Path: "/", 31 | RequestContext: events.ALBTargetGroupRequestContext{ 32 | ELB: events.ELBContext{TargetGroupArn: " ad"}, 33 | }} 34 | 35 | resp, err := adapter.ProxyWithContext(context.Background(), req) 36 | 37 | Expect(err).To(BeNil()) 38 | Expect(resp.StatusCode).To(Equal(200)) 39 | 40 | resp, err = adapter.Proxy(req) 41 | 42 | Expect(err).To(BeNil()) 43 | Expect(resp.StatusCode).To(Equal(200)) 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /handlerfunc/adapter_test.go: -------------------------------------------------------------------------------- 1 | package handlerfunc_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/aws/aws-lambda-go/events" 10 | "github.com/awslabs/aws-lambda-go-api-proxy/handlerfunc" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("HandlerFuncAdapter tests", func() { 17 | Context("Simple ping request", func() { 18 | It("Proxies the event correctly", func() { 19 | log.Println("Starting test") 20 | 21 | handler := func(w http.ResponseWriter, req *http.Request) { 22 | w.Header().Add("unfortunately-required-header", "") 23 | fmt.Fprintf(w, "Go Lambda!!") 24 | } 25 | 26 | adapter := handlerfunc.New(handler) 27 | 28 | req := events.APIGatewayProxyRequest{ 29 | Path: "/ping", 30 | HTTPMethod: "GET", 31 | } 32 | 33 | resp, err := adapter.ProxyWithContext(context.Background(), req) 34 | 35 | Expect(err).To(BeNil()) 36 | Expect(resp.StatusCode).To(Equal(200)) 37 | 38 | resp, err = adapter.Proxy(req) 39 | 40 | Expect(err).To(BeNil()) 41 | Expect(resp.StatusCode).To(Equal(200)) 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /handlerfunc/adapterv2.go: -------------------------------------------------------------------------------- 1 | package handlerfunc 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" 7 | ) 8 | 9 | type HandlerFuncAdapterV2 = httpadapter.HandlerAdapterV2 10 | 11 | func NewV2(handlerFunc http.HandlerFunc) *HandlerFuncAdapterV2 { 12 | return httpadapter.NewV2(handlerFunc) 13 | } 14 | -------------------------------------------------------------------------------- /handlerfunc/adapterv2_test.go: -------------------------------------------------------------------------------- 1 | package handlerfunc_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/aws/aws-lambda-go/events" 10 | "github.com/awslabs/aws-lambda-go-api-proxy/handlerfunc" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("HandlerFuncAdapter tests", func() { 17 | Context("Simple ping request", func() { 18 | It("Proxies the event correctly", func() { 19 | log.Println("Starting test") 20 | 21 | handler := func(w http.ResponseWriter, req *http.Request) { 22 | w.Header().Add("unfortunately-required-header", "") 23 | fmt.Fprintf(w, "Go Lambda!!") 24 | } 25 | 26 | adapter := handlerfunc.NewV2(handler) 27 | 28 | req := events.APIGatewayV2HTTPRequest{ 29 | RequestContext: events.APIGatewayV2HTTPRequestContext{ 30 | HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{ 31 | Method: http.MethodGet, 32 | Path: "/ping", 33 | }, 34 | }, 35 | } 36 | 37 | resp, err := adapter.ProxyWithContext(context.Background(), req) 38 | 39 | Expect(err).To(BeNil()) 40 | Expect(resp.StatusCode).To(Equal(200)) 41 | 42 | resp, err = adapter.Proxy(req) 43 | 44 | Expect(err).To(BeNil()) 45 | Expect(resp.StatusCode).To(Equal(200)) 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /handlerfunc/handlerfunc_suite_test.go: -------------------------------------------------------------------------------- 1 | package handlerfunc_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestHandlerFunc(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "HandlerFuncAdapter Suite") 13 | } 14 | -------------------------------------------------------------------------------- /httpadapter/adapter.go: -------------------------------------------------------------------------------- 1 | package httpadapter 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/aws/aws-lambda-go/events" 8 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 9 | ) 10 | 11 | type HandlerAdapter struct { 12 | core.RequestAccessor 13 | handler http.Handler 14 | } 15 | 16 | func New(handler http.Handler) *HandlerAdapter { 17 | return &HandlerAdapter{ 18 | handler: handler, 19 | } 20 | } 21 | 22 | // Proxy receives an API Gateway proxy event, transforms it into an http.Request 23 | // object, and sends it to the http.HandlerFunc for routing. 24 | // It returns a proxy response object generated from the http.Handler. 25 | func (h *HandlerAdapter) Proxy(event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 26 | req, err := h.ProxyEventToHTTPRequest(event) 27 | return h.proxyInternal(req, err) 28 | } 29 | 30 | // ProxyWithContext receives context and an API Gateway proxy event, 31 | // transforms them into an http.Request object, and sends it to the http.Handler for routing. 32 | // It returns a proxy response object generated from the http.ResponseWriter. 33 | func (h *HandlerAdapter) ProxyWithContext(ctx context.Context, event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 34 | req, err := h.EventToRequestWithContext(ctx, event) 35 | return h.proxyInternal(req, err) 36 | } 37 | 38 | func (h *HandlerAdapter) proxyInternal(req *http.Request, err error) (events.APIGatewayProxyResponse, error) { 39 | if err != nil { 40 | return core.GatewayTimeout(), core.NewLoggedError("Could not convert proxy event to request: %v", err) 41 | } 42 | 43 | w := core.NewProxyResponseWriter() 44 | h.handler.ServeHTTP(http.ResponseWriter(w), req) 45 | 46 | resp, err := w.GetProxyResponse() 47 | if err != nil { 48 | return core.GatewayTimeout(), core.NewLoggedError("Error while generating proxy response: %v", err) 49 | } 50 | 51 | return resp, nil 52 | } 53 | -------------------------------------------------------------------------------- /httpadapter/adapterALB.go: -------------------------------------------------------------------------------- 1 | package httpadapter 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/aws/aws-lambda-go/events" 8 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 9 | ) 10 | 11 | type HandlerAdapterALB struct { 12 | core.RequestAccessorALB 13 | handler http.Handler 14 | } 15 | 16 | func NewALB(handler http.Handler) *HandlerAdapterALB { 17 | return &HandlerAdapterALB{ 18 | handler: handler, 19 | } 20 | } 21 | 22 | // Proxy receives an ALB Target Group proxy event, transforms it into an http.Request 23 | // object, and sends it to the http.HandlerFunc for routing. 24 | // It returns a proxy response object generated from the http.ResponseWriter. 25 | func (h *HandlerAdapterALB) Proxy(event events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) { 26 | req, err := h.ProxyEventToHTTPRequest(event) 27 | return h.proxyInternal(req, err) 28 | } 29 | 30 | // ProxyWithContext receives context and an ALB proxy event, 31 | // transforms them into an http.Request object, and sends it to the http.Handler for routing. 32 | // It returns a proxy response object generated from the http.ResponseWriter. 33 | func (h *HandlerAdapterALB) ProxyWithContext(ctx context.Context, event events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) { 34 | req, err := h.EventToRequestWithContext(ctx, event) 35 | return h.proxyInternal(req, err) 36 | } 37 | 38 | func (h *HandlerAdapterALB) proxyInternal(req *http.Request, err error) (events.ALBTargetGroupResponse, error) { 39 | if err != nil { 40 | return core.GatewayTimeoutALB(), core.NewLoggedError("Could not convert proxy event to request: %v", err) 41 | } 42 | 43 | w := core.NewProxyResponseWriterALB() 44 | h.handler.ServeHTTP(http.ResponseWriter(w), req) 45 | 46 | resp, err := w.GetProxyResponse() 47 | if err != nil { 48 | return core.GatewayTimeoutALB(), core.NewLoggedError("Error while generating proxy response: %v", err) 49 | } 50 | 51 | return resp, nil 52 | } 53 | -------------------------------------------------------------------------------- /httpadapter/adapterALB_test.go: -------------------------------------------------------------------------------- 1 | package httpadapter_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/aws/aws-lambda-go/events" 10 | "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("HandlerFuncAdapter tests", func() { 17 | Context("Simple ping request", func() { 18 | It("Proxies the event correctly", func() { 19 | log.Println("Starting test") 20 | 21 | var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 22 | w.Header().Add("unfortunately-required-header", "") 23 | fmt.Fprintf(w, "Go Lambda!!") 24 | }) 25 | 26 | adapter := httpadapter.NewV2(handler) 27 | 28 | req := events.APIGatewayV2HTTPRequest{ 29 | RequestContext: events.APIGatewayV2HTTPRequestContext{ 30 | HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{ 31 | Method: http.MethodGet, 32 | Path: "/ping", 33 | }, 34 | }, 35 | } 36 | 37 | resp, err := adapter.ProxyWithContext(context.Background(), req) 38 | 39 | Expect(err).To(BeNil()) 40 | Expect(resp.StatusCode).To(Equal(200)) 41 | 42 | resp, err = adapter.Proxy(req) 43 | 44 | Expect(err).To(BeNil()) 45 | Expect(resp.StatusCode).To(Equal(200)) 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /httpadapter/adapter_test.go: -------------------------------------------------------------------------------- 1 | package httpadapter_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/aws/aws-lambda-go/events" 10 | "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("HTTPAdapter tests", func() { 17 | Context("Simple ping request", func() { 18 | It("Proxies the event correctly", func() { 19 | log.Println("Starting test") 20 | 21 | var httpHandler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 22 | w.Header().Add("unfortunately-required-header", "") 23 | fmt.Fprintf(w, "Go Lambda!!") 24 | }) 25 | 26 | adapter := httpadapter.New(httpHandler) 27 | 28 | req := events.APIGatewayProxyRequest{ 29 | Path: "/ping", 30 | HTTPMethod: "GET", 31 | } 32 | 33 | resp, err := adapter.ProxyWithContext(context.Background(), req) 34 | 35 | Expect(err).To(BeNil()) 36 | Expect(resp.StatusCode).To(Equal(200)) 37 | 38 | resp, err = adapter.Proxy(req) 39 | 40 | Expect(err).To(BeNil()) 41 | Expect(resp.StatusCode).To(Equal(200)) 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /httpadapter/adapterv2.go: -------------------------------------------------------------------------------- 1 | package httpadapter 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/aws/aws-lambda-go/events" 8 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 9 | ) 10 | 11 | type HandlerAdapterV2 struct { 12 | core.RequestAccessorV2 13 | handler http.Handler 14 | } 15 | 16 | func NewV2(handler http.Handler) *HandlerAdapterV2 { 17 | return &HandlerAdapterV2{ 18 | handler: handler, 19 | } 20 | } 21 | 22 | // Proxy receives an API Gateway proxy event, transforms it into an http.Request 23 | // object, and sends it to the http.HandlerFunc for routing. 24 | // It returns a proxy response object generated from the http.ResponseWriter. 25 | func (h *HandlerAdapterV2) Proxy(event events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 26 | req, err := h.ProxyEventToHTTPRequest(event) 27 | return h.proxyInternal(req, err) 28 | } 29 | 30 | // ProxyWithContext receives context and an API Gateway proxy event, 31 | // transforms them into an http.Request object, and sends it to the http.Handler for routing. 32 | // It returns a proxy response object generated from the http.ResponseWriter. 33 | func (h *HandlerAdapterV2) ProxyWithContext(ctx context.Context, event events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 34 | req, err := h.EventToRequestWithContext(ctx, event) 35 | return h.proxyInternal(req, err) 36 | } 37 | 38 | func (h *HandlerAdapterV2) proxyInternal(req *http.Request, err error) (events.APIGatewayV2HTTPResponse, error) { 39 | if err != nil { 40 | return core.GatewayTimeoutV2(), core.NewLoggedError("Could not convert proxy event to request: %v", err) 41 | } 42 | 43 | w := core.NewProxyResponseWriterV2() 44 | h.handler.ServeHTTP(http.ResponseWriter(w), req) 45 | 46 | resp, err := w.GetProxyResponse() 47 | if err != nil { 48 | return core.GatewayTimeoutV2(), core.NewLoggedError("Error while generating proxy response: %v", err) 49 | } 50 | 51 | return resp, nil 52 | } 53 | -------------------------------------------------------------------------------- /httpadapter/adapterv2_test.go: -------------------------------------------------------------------------------- 1 | package httpadapter_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/aws/aws-lambda-go/events" 10 | "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("HandlerFuncAdapter tests", func() { 17 | Context("Simple ping request", func() { 18 | It("Proxies the event correctly", func() { 19 | log.Println("Starting test") 20 | 21 | var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 22 | w.Header().Add("unfortunately-required-header", "") 23 | fmt.Fprintf(w, "Go Lambda!!") 24 | }) 25 | 26 | adapter := httpadapter.NewV2(handler) 27 | 28 | req := events.APIGatewayV2HTTPRequest{ 29 | RequestContext: events.APIGatewayV2HTTPRequestContext{ 30 | HTTP: events.APIGatewayV2HTTPRequestContextHTTPDescription{ 31 | Method: http.MethodGet, 32 | Path: "/ping", 33 | }, 34 | }, 35 | } 36 | 37 | resp, err := adapter.ProxyWithContext(context.Background(), req) 38 | 39 | Expect(err).To(BeNil()) 40 | Expect(resp.StatusCode).To(Equal(200)) 41 | 42 | resp, err = adapter.Proxy(req) 43 | 44 | Expect(err).To(BeNil()) 45 | Expect(resp.StatusCode).To(Equal(200)) 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /httpadapter/handlerfunc_suite_test.go: -------------------------------------------------------------------------------- 1 | package httpadapter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestHTTPAdapter(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "HttpAdapter Suite") 13 | } 14 | -------------------------------------------------------------------------------- /iris/adapter.go: -------------------------------------------------------------------------------- 1 | // Package irisLambda add Iris support for the aws-serverless-go-api library. 2 | // Uses the core package behind the scenes and exposes the New method to 3 | // get a new instance and Proxy method to send request to the iris.Application. 4 | package irisadapter 5 | 6 | import ( 7 | "context" 8 | "net/http" 9 | 10 | "github.com/aws/aws-lambda-go/events" 11 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 12 | "github.com/kataras/iris/v12" 13 | ) 14 | 15 | // IrisLambda makes it easy to send API Gateway proxy events to a iris.Application. 16 | // The library transforms the proxy event into an HTTP request and then 17 | // creates a proxy response object from the http.ResponseWriter 18 | type IrisLambda struct { 19 | core.RequestAccessor 20 | 21 | application *iris.Application 22 | } 23 | 24 | // New creates a new instance of the IrisLambda object. 25 | // Receives an initialized *iris.Application object - normally created with iris.Default(). 26 | // It returns the initialized instance of the IrisLambda object. 27 | func New(app *iris.Application) *IrisLambda { 28 | return &IrisLambda{application: app} 29 | } 30 | 31 | // Proxy receives an API Gateway proxy event, transforms it into an http.Request 32 | // object, and sends it to the iris.Application for routing. 33 | // It returns a proxy response object generated from the http.ResponseWriter. 34 | func (i *IrisLambda) Proxy(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 35 | irisRequest, err := i.ProxyEventToHTTPRequest(req) 36 | return i.proxyInternal(irisRequest, err) 37 | } 38 | 39 | // ProxyWithContext receives context and an API Gateway proxy event, 40 | // transforms them into an http.Request object, and sends it to the iris.Application for routing. 41 | // It returns a proxy response object generated from the http.ResponseWriter. 42 | func (i *IrisLambda) ProxyWithContext(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 43 | irisRequest, err := i.EventToRequestWithContext(ctx, req) 44 | return i.proxyInternal(irisRequest, err) 45 | } 46 | 47 | func (i *IrisLambda) proxyInternal(req *http.Request, err error) (events.APIGatewayProxyResponse, error) { 48 | if err != nil { 49 | return core.GatewayTimeout(), core.NewLoggedError("Could not convert proxy event to request: %v", err) 50 | } 51 | 52 | if err := i.application.Build(); err != nil { 53 | return core.GatewayTimeout(), core.NewLoggedError("Iris set up failed: %v", err) 54 | } 55 | 56 | respWriter := core.NewProxyResponseWriter() 57 | i.application.ServeHTTP(http.ResponseWriter(respWriter), req) 58 | 59 | proxyResponse, err := respWriter.GetProxyResponse() 60 | if err != nil { 61 | return core.GatewayTimeout(), core.NewLoggedError("Error while generating proxy response: %v", err) 62 | } 63 | 64 | return proxyResponse, nil 65 | } 66 | -------------------------------------------------------------------------------- /iris/iris_suite_test.go: -------------------------------------------------------------------------------- 1 | package irisadapter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestEcho(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Iris Suite") 13 | } 14 | -------------------------------------------------------------------------------- /iris/irislambda_test.go: -------------------------------------------------------------------------------- 1 | package irisadapter_test 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/aws/aws-lambda-go/events" 8 | irisadapter "github.com/awslabs/aws-lambda-go-api-proxy/iris" 9 | "github.com/kataras/iris/v12" 10 | 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | var _ = Describe("IrisLambda tests", func() { 16 | Context("Simple ping request", func() { 17 | It("Proxies the event correctly", func() { 18 | log.Println("Starting test") 19 | 20 | app := iris.New() 21 | app.Get("/ping", func(ctx iris.Context) { 22 | log.Println("Handler!!") 23 | ctx.WriteString("pong") 24 | }) 25 | 26 | adapter := irisadapter.New(app) 27 | 28 | req := events.APIGatewayProxyRequest{ 29 | Path: "/ping", 30 | HTTPMethod: "GET", 31 | } 32 | 33 | resp, err := adapter.ProxyWithContext(context.Background(), req) 34 | 35 | Expect(err).To(BeNil()) 36 | Expect(resp.StatusCode).To(Equal(200)) 37 | 38 | resp, err = adapter.Proxy(req) 39 | 40 | Expect(err).To(BeNil()) 41 | Expect(resp.StatusCode).To(Equal(200)) 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /negroni/adapter.go: -------------------------------------------------------------------------------- 1 | package negroniadapter 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/aws/aws-lambda-go/events" 8 | "github.com/awslabs/aws-lambda-go-api-proxy/core" 9 | "github.com/urfave/negroni" 10 | ) 11 | 12 | type NegroniAdapter struct { 13 | core.RequestAccessor 14 | n *negroni.Negroni 15 | } 16 | 17 | func New(n *negroni.Negroni) *NegroniAdapter { 18 | return &NegroniAdapter{ 19 | n: n, 20 | } 21 | } 22 | 23 | // Proxy receives an API Gateway proxy event, transforms it into an http.Request 24 | // object, and sends it to the negroni.Negroni for routing. 25 | // It returns a proxy response object generated from the http.Handler. 26 | func (h *NegroniAdapter) Proxy(event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 27 | req, err := h.ProxyEventToHTTPRequest(event) 28 | return h.proxyInternal(req, err) 29 | } 30 | 31 | // ProxyWithContext receives context and an API Gateway proxy event, 32 | // transforms them into an http.Request object, and sends it to the negroni.Negroni for routing. 33 | // It returns a proxy response object generated from the http.ResponseWriter. 34 | func (h *NegroniAdapter) ProxyWithContext(ctx context.Context, event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 35 | req, err := h.EventToRequestWithContext(ctx, event) 36 | return h.proxyInternal(req, err) 37 | } 38 | 39 | func (h *NegroniAdapter) proxyInternal(req *http.Request, err error) (events.APIGatewayProxyResponse, error) { 40 | if err != nil { 41 | return core.GatewayTimeout(), core.NewLoggedError("Could not convert proxy event to request: %v", err) 42 | } 43 | 44 | w := core.NewProxyResponseWriter() 45 | h.n.ServeHTTP(http.ResponseWriter(w), req) 46 | 47 | resp, err := w.GetProxyResponse() 48 | if err != nil { 49 | return core.GatewayTimeout(), core.NewLoggedError("Error while generating proxy response: %v", err) 50 | } 51 | 52 | return resp, nil 53 | } 54 | -------------------------------------------------------------------------------- /negroni/adapter_test.go: -------------------------------------------------------------------------------- 1 | package negroniadapter_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/aws/aws-lambda-go/events" 10 | negroniadapter "github.com/awslabs/aws-lambda-go-api-proxy/negroni" 11 | "github.com/urfave/negroni" 12 | 13 | . "github.com/onsi/ginkgo" 14 | . "github.com/onsi/gomega" 15 | ) 16 | 17 | var _ = Describe("NegroniAdapter tests", func() { 18 | Context("Tests multiple handlers", func() { 19 | It("Proxies the event correctly", func() { 20 | log.Println("Starting test") 21 | 22 | homeHandler := func(w http.ResponseWriter, req *http.Request) { 23 | w.Header().Add("unfortunately-required-header", "") 24 | fmt.Fprintf(w, "Home Page") 25 | } 26 | 27 | productsHandler := func(w http.ResponseWriter, req *http.Request) { 28 | w.Header().Add("unfortunately-required-header", "") 29 | fmt.Fprintf(w, "Products Page") 30 | } 31 | 32 | mux := http.NewServeMux() 33 | mux.HandleFunc("/", homeHandler) 34 | mux.HandleFunc("/products", productsHandler) 35 | 36 | n := negroni.New() 37 | n.UseHandler(mux) 38 | 39 | adapter := negroniadapter.New(n) 40 | 41 | homePageReq := events.APIGatewayProxyRequest{ 42 | Path: "/", 43 | HTTPMethod: "GET", 44 | } 45 | 46 | homePageResp, homePageReqErr := adapter.ProxyWithContext(context.Background(), homePageReq) 47 | 48 | Expect(homePageReqErr).To(BeNil()) 49 | Expect(homePageResp.StatusCode).To(Equal(200)) 50 | Expect(homePageResp.Body).To(Equal("Home Page")) 51 | 52 | productsPageReq := events.APIGatewayProxyRequest{ 53 | Path: "/products", 54 | HTTPMethod: "GET", 55 | } 56 | 57 | productsPageResp, productsPageReqErr := adapter.Proxy(productsPageReq) 58 | 59 | Expect(productsPageReqErr).To(BeNil()) 60 | Expect(productsPageResp.StatusCode).To(Equal(200)) 61 | Expect(productsPageResp.Body).To(Equal("Products Page")) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /negroni/negroni_suite_test.go: -------------------------------------------------------------------------------- 1 | package negroniadapter_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestNegroni(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "NegroniAdapter Suite") 13 | } 14 | -------------------------------------------------------------------------------- /sample/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/awslabs/aws-lambda-go-api-proxy-sample 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/aws/aws-lambda-go v1.41.0 7 | github.com/awslabs/aws-lambda-go-api-proxy v0.16.0 8 | github.com/gin-gonic/gin v1.9.1 9 | github.com/google/uuid v1.3.0 10 | github.com/kr/pretty v0.3.0 // indirect 11 | github.com/rogpeppe/go-internal v1.8.0 // indirect 12 | ) 13 | 14 | replace ( 15 | golang.org/x/crypto => golang.org/x/crypto v0.6.0 16 | gopkg.in/yaml.v2 v2.2.2 => gopkg.in/yaml.v2 v2.2.8 17 | gopkg.in/yaml.v2 v2.2.4 => gopkg.in/yaml.v2 v2.2.8 18 | ) 19 | -------------------------------------------------------------------------------- /sample/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "strconv" 8 | 9 | "github.com/aws/aws-lambda-go/events" 10 | "github.com/aws/aws-lambda-go/lambda" 11 | ginadapter "github.com/awslabs/aws-lambda-go-api-proxy/gin" 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | var ginLambda *ginadapter.GinLambda 16 | 17 | // Handler is the main entry point for Lambda. Receives a proxy request and 18 | // returns a proxy response 19 | func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 20 | if ginLambda == nil { 21 | // stdout and stderr are sent to AWS CloudWatch Logs 22 | log.Printf("Gin cold start") 23 | r := gin.Default() 24 | r.GET("/pets", getPets) 25 | r.GET("/pets/:id", getPet) 26 | r.POST("/pets", createPet) 27 | 28 | ginLambda = ginadapter.New(r) 29 | } 30 | 31 | return ginLambda.ProxyWithContext(ctx, req) 32 | } 33 | 34 | func main() { 35 | lambda.Start(Handler) 36 | } 37 | 38 | func getPets(c *gin.Context) { 39 | limit := 10 40 | if c.Query("limit") != "" { 41 | newLimit, err := strconv.Atoi(c.Query("limit")) 42 | if err != nil { 43 | limit = 10 44 | } else { 45 | limit = newLimit 46 | } 47 | } 48 | if limit > 50 { 49 | limit = 50 50 | } 51 | pets := make([]Pet, limit) 52 | 53 | for i := 0; i < limit; i++ { 54 | pets[i] = getRandomPet() 55 | } 56 | 57 | c.JSON(200, pets) 58 | } 59 | 60 | func getPet(c *gin.Context) { 61 | petID := c.Param("id") 62 | randomPet := getRandomPet() 63 | randomPet.ID = petID 64 | c.JSON(200, randomPet) 65 | } 66 | 67 | func createPet(c *gin.Context) { 68 | newPet := Pet{} 69 | err := c.BindJSON(&newPet) 70 | 71 | if err != nil { 72 | return 73 | } 74 | 75 | newPet.ID = getUUID() 76 | c.JSON(http.StatusAccepted, newPet) 77 | } 78 | -------------------------------------------------------------------------------- /sample/pets.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/google/uuid" 9 | ) 10 | 11 | var breeds = []string{"Afghan Hound", "Beagle", "Bernese Mountain Dog", "Bloodhound", "Dalmatian", "Jack Russell Terrier", "Norwegian Elkhound"} 12 | var names = []string{"Bailey", "Bella", "Max", "Lucy", "Charlie", "Molly", "Buddy", "Daisy", "Rocky", "Maggie", "Jake", "Sophie", "Jack", "Sadie", "Toby", "Chloe", "Cody", "Bailey", "Buster", "Lola", "Duke", "Zoe", "Cooper", "Abby", "Riley", "Ginger", "Harley", "Roxy", "Bear", "Gracie", "Tucker", "Coco", "Murphy", "Sasha", "Lucky", "Lily", "Oliver", "Angel", "Sam", "Princess", "Oscar", "Emma", "Teddy", "Annie", "Winston", "Rosie"} 13 | 14 | type Pet struct { 15 | ID string `json:"id"` 16 | Breed string `json:"breed"` 17 | Name string `json:"name"` 18 | DateOfBirth time.Time `json:"dateOfBirth"` 19 | } 20 | 21 | func getRandomPet() Pet { 22 | pet := Pet{} 23 | 24 | pet.ID = getUUID() 25 | pet.Breed = randomBreed() 26 | pet.Name = randomName() 27 | 28 | pet.DateOfBirth = randomDate() 29 | 30 | return pet 31 | } 32 | 33 | func randomDate() time.Time { 34 | now := time.Now() 35 | start := now.AddDate(-15, 0, 0) 36 | delta := now.Unix() - start.Unix() 37 | 38 | sec := rand.Int63n(delta) + start.Unix() 39 | return time.Unix(sec, 0) 40 | } 41 | 42 | func randomBreed() string { 43 | return breeds[random(0, len(breeds))] 44 | } 45 | 46 | func randomName() string { 47 | return names[random(0, len(names))] 48 | } 49 | 50 | func random(min int, max int) int { 51 | return rand.Intn(max-min) + min 52 | } 53 | 54 | func getUUID() string { 55 | uuid, err := uuid.NewRandom() 56 | if err != nil { 57 | log.Fatal(err) 58 | return "" 59 | } 60 | return uuid.String() 61 | } 62 | -------------------------------------------------------------------------------- /sample/sam.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Example Lambda Gin 4 | Resources: 5 | SampleFunction: 6 | Type: AWS::Serverless::Function 7 | Properties: 8 | Handler: main 9 | CodeUri: main.zip 10 | Runtime: go1.x 11 | MemorySize: 128 12 | Policies: AWSLambdaBasicExecutionRole 13 | Timeout: 3 14 | Events: 15 | GetResource: 16 | Type: Api 17 | Properties: 18 | Path: /{proxy+} 19 | Method: any 20 | 21 | Outputs: 22 | SampleGinApi: 23 | Description: URL for application 24 | Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/pets' 25 | Export: 26 | Name: SampleGinApi 27 | --------------------------------------------------------------------------------