How to upload and parse multiple files to a GoLang API behind API Gateway running on AWS Lambda?

Viewed 27

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 Answers

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.

  1. Update your router and add a new endpoint to upload to: router.Route("POST", "/uploadsessions", session.UploadSessionsLambda)
  2. Create a file 'hello.txt' - contents hello human
  3. Create a file 'goodbye.txt' - contents goodbye human
  4. Create a new request in POSTMAN to test: required postman setup
  5. 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 substitute bufio.NewReader with csv.NewReader in 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
}
Related