How to Serve Golang Netlify Functions Locally

2020-11-13 | Shiplet

I've been writing a lot of Netlify (AWS Lambda) functions lately, and I got tired of having to create an entirely separate control flow in order to test my logic. I wanted to be able to run these functions locally just like the JavaScript peeps.

So I created this little localServer package! It's not published in git anywhere yet, so I'll update this if/when that happens. But, the gist of what's going on is: I made a stdlib golang http server that captures incoming http events, mocks them to the events.APIGatewayProxyRequest and events.APIGatewayProxyResponse objects which the aws-lambda-go/events package looks for, and then passes those objects to the main AWS Lambda handler.

So far it only supports GET, POST, and OPTIONS requests (it throws Access-Control-Allow-Origin: * on everything), and it only mocks the proxy items that I'm actually using, like Method, QueryStringParameters, Body, and so forth. But it's working for me so far, and that's as good a place to start as any.

I'm sure the function signatures could stand some cleaning-up, but I'll get there when I get there.

Here's the code for it:

package localServer

import (
	"fmt"
	"github.com/aws/aws-lambda-go/events"
	"io/ioutil"
	"net/http"
)

func LocalServer(endpointMock string, port int, proxyHandler func(event *events.APIGatewayProxyRequest)(*events.APIGatewayProxyResponse, error)) {
	http.HandleFunc(endpointMock, handleLocal(proxyHandler))
	log.Printf("listening on port %d", port)
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
}

func handleLocal(proxyHandler func(event *events.APIGatewayProxyRequest)(*events.APIGatewayProxyResponse, error)) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		var body []byte
		keys := make(map[string]string)
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
		w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
		w.Header().Set("Content-Type", "text/html; charset=utf-8")

		if r.Method == http.MethodOptions {
			return
		}

		if r.Method == http.MethodPost {
			parsed, err := ioutil.ReadAll(r.Body)
			body = parsed
			if err != nil {
				http.Error(w, "failed to read body", http.StatusInternalServerError)
				return
			}
		}

		if r.Method == http.MethodGet {
			queryKeys := r.URL.Query()
			for k, v := range queryKeys {
				keys[k] = v[0]
			}
		}

		request := &events.APIGatewayProxyRequest{
			HTTPMethod:            r.Method,
			RequestContext:        events.APIGatewayProxyRequestContext{},
			Body:                  string(body),
		}

		if len(keys) > 0 {
			request.QueryStringParameters = keys
		}

		res, err := proxyHandler(request)
		if err != nil {
			http.Error(w, fmt.Sprintf("failed to convert request to lambda event: %s", err), res.StatusCode)
			return
		}

		w.WriteHeader(res.StatusCode)
		fmt.Fprintf(w, res.Body)
	}
}

And then to use it in your Netlify function's main.go, add it to a sibling directory called localServer, and invoke it like so:

package main

import "main/localServer"

func main() {
	//lambda.Start(HandleEvent)
	localServer.LocalServer("/YOUR_ENDPOINT", 3000, HandleEvent)
}

The final file-tree should look like:

functionRoot
└── main.go
      localServer
      └──  localServer.go

And voila! You've got a local server you can test & debug against. Just make sure to move everything back over to the lambda.Start() once you're finished. Also, it's not COMPLETELY like the JavaScript package, because you'll have to run this on a function-by-function basis (just make sure you update your port), rather than at the top of your functions directory, or even better, the project root.

Future work!