I have a GoLang API behind API Gateway running on AWS Lambda. I'd like to create a new endpoint to handle multiple file uploads. I would like that endpoint to receive and parse those files before returning a result based on the file contents.
1 Answer
This took me a few hours to figure out so I'd love to share my solution with everyone. AWS Lambda uses Handlers which have a specific format. That format is:
func handler(req events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error) {
So all of our logic must be contained within a function with this method signature.
- Update your router and add a new endpoint to upload to:
router.Route("POST", "/uploadsessions", session.UploadSessionsLambda) - Create a file 'hello.txt' - contents
hello human - Create a file 'goodbye.txt' - contents
goodbye human - Create a new request in
POSTMANto test:
- Implement the API Gateway / AWS Lambda Proxy function handler
func UploadSessionsLambda(_ context.Context, lambdaReq events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {. Below is my implementation. It still needs some tweaking but works as-is. I'll be parsing CSVs in which case you substitutebufio.NewReaderwithcsv.NewReaderin the below code.
const uploadLimitBytes = 50000000 // 50 megabytes type UploadResponse struct { Concat string } func UploadSessionsLambda(_ context.Context, lambdaReq events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { contentType := lambdaReq.Headers["Content-Type"] if contentType == "" { return HandleHTTPError(http.StatusBadRequest, fmt.Errorf("request contained no Content-Type header")) } _, params, err := mime.ParseMediaType(contentType) if err != nil { return HandleHTTPError(http.StatusBadRequest, err) } boundary := params["boundary"] if boundary == "" { return HandleHTTPError(http.StatusBadRequest, fmt.Errorf("request contained no boundary value to parse from Content-Type headers")) } stringReader := strings.NewReader(lambdaReq.Body) multipartReader := multipart.NewReader(stringReader, boundary) form, err := multipartReader.ReadForm(uploadLimitBytes) if err != nil { return HandleHTTPError(http.StatusBadRequest, err) } var sb strings.Builder for currentFileName := range form.File { // anonymous file handler func allows for calling defer .Close() httpStatus, handlerErr := func(fileName string) (int, error) { currentFileHeader := form.File[currentFileName][0] currentFile, openErr := currentFileHeader.Open() if openErr != nil { return http.StatusInternalServerError, openErr } defer currentFile.Close() // figure out how to trap this error bufferedReader := bufio.NewReader(currentFile) for { line, _, readLineErr := bufferedReader.ReadLine() if readLineErr == io.EOF { break } sb.Write(line) } return http.StatusOK, nil }(currentFileName) if handlerErr != nil { return HandleHTTPError(httpStatus, handlerErr) } } return MarshalSuccess(&UploadResponse{Concat: sb.String()}) } Helper functions:
func HandleHTTPError(httpStatus int, err error) (events.APIGatewayProxyResponse, error) { httpErr := HTTPError{ Status: httpStatus, Message: err.Error(), } if httpErr.Status >= 500 && !ExposeServerErrors { httpErr.Message = http.StatusText(httpErr.Status) } return MarshalResponse(httpErr.Status, nil, httpErr) } func MarshalSuccess(data interface{}) (events.APIGatewayProxyResponse, error) { return MarshalResponse(http.StatusOK, nil, data) } // MarshalResponse generated an events.APIGatewayProxyResponse object that can // be directly returned via the lambda's handler function. It receives an HTTP // status code for the response, a map of HTTP headers (can be empty or nil), // and a value (probably a struct) representing the response body. This value // will be marshaled to JSON (currently without base 64 encoding). func MarshalResponse(httpStatus int, headers map[string]string, data interface{}) ( events.APIGatewayProxyResponse, error, ) { b, err := json.Marshal(data) if err != nil { httpStatus = http.StatusInternalServerError b = []byte(`{"code":500,"message":"the server has encountered an unexpected error"}`) } if headers == nil { headers = make(map[string]string) } return events.APIGatewayProxyResponse{ StatusCode: httpStatus, IsBase64Encoded: false, Headers: headers, Body: string(b), }, nil }