├── .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 [](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 |
--------------------------------------------------------------------------------