ChatGPT Part 2

The previous part in this series introduced ChatGPT and explained my motivations for testing it out. One of the coolest features of the tool is that it will remember previous topics in the conversation and apply them in future prompts. So you don't need to get it right the first try, you can rephrase and try again.

Since I learn best when the knowledge is applied, I asked ChatGPT to help me with a problem I was struggling with for the past 4 days. I was trying to come up with an elegant solution to using AWS application load balancers to process HTTP requests with Lambda functions, and I wanted to use the Go programming language. However, doing HTTP path multiplexing is something that the standard Go muxer is not good or performant at. For instance, the default muxer can handle GET /blog perfectly, but it cannot handle GET /blog/:id because it doesn't know how to parse path parameters. I could start parsing those path parameters with regex, but then I'd start having regex problems, and I'm certain it wouldn't be performant or battle-tested. That's where muxers like Gin or Chi or Gorilla come in. I wanted to write the Lambda function to pass the ALB request to a Gin backend and have it mux for me. The problem was that constructing an HTTP response struct by hand is not something people do, and I was struggling to write it properly. I tried using net/http/httptest/ResponseRecorder and it worked, but that struct is for unit testing and it felt wrong to adapt it for – what I would expect to be – production code. None of my solutions worked, so I asked ChatGPT to help me.

It took me a few tries to think of the correct prompt. I eventually got it right. Here is the conversation that ensued.

Prompt

A /ping HTTP request is sent to an Amazon Load Balancer, which then sends the events to a Lambda function for processing. How do I translate an ALBTargetGroupRequest payload in a Lambda function to a Gin Gonic backend?

ChatGPT Response

To translate an ALBTargetGroupRequest payload in a Lambda function to a Gin Gonic backend, you will need to write code in the Lambda function that extracts the relevant information from the ALB request, such as the HTTP method, headers, and body, and then constructs a Gin Gonic request with that information. You can then send the constructed request to the Gin Gonic backend and return its response back to the ALB.

Here is an example in Go, using the Gin Gonic and AWS SDK for Go libraries:

package main

import (
    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/gin-gonic/gin"
    "net/http"
)

func handleRequest(albEvent events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) {
    ginContext, engine := gin.CreateTestContext(nil)
    req, err := http.NewRequest(albEvent.HTTPMethod, albEvent.Path, nil)
    if err != nil {
        return events.ALBTargetGroupResponse{StatusCode: http.StatusBadRequest}, err
    }
    for headerName, headerValues := range albEvent.Headers {
        for _, headerValue := range headerValues {
            req.Header.Add(headerName, headerValue)
        }
    }
    ginContext.Request = req
    engine.ServeHTTP(ginContext)

    return events.ALBTargetGroupResponse{
        StatusCode: ginContext.Writer.Status(),
        Body:       ginContext.Writer.Body.String(),
        Headers: map[string]string{
            "Content-Type": ginContext.GetHeader("Content-Type"),
        },
    }, nil
}

func main() {
    lambda.Start(handleRequest)
}

Review

This is interesting. ChatGPT took only my prompt and produced a nearly perfect set of Go code for me. Since my own research was failed me in this situation, I doubt that people have written must of this, so it's not like ChatGPT went to Google and grabbed it from StackOverflow.

But here ChatGPT found a solution that didn't rely on recreating an HTTP environment. That's beacuse my prompt stated the "goal", rather than the "method". In programming terms, my prompt was being declarative rather than being imperative. And this allowed ChatGPT to come up with a solution that was novel (to me) and something that worked and was sustainable.

I had not thought about using Gin contexts. I had been so fixated on recreating HTTP requests and responses, I didn't look at how else Gin handles muxing. That said, I did notice something wrong in it output. The ServeHTTP method does not take a Gin context struct, but rather an HTTPResponseWriter and HTTPRequest. I know because that's what I've been struggling with for days. I validated my theory by looking at the Gin code on Github. Sure enough, I was right. The code ChatGPT produced would never have compiled.

So I followed up with ChatGPT again to see if it could find a better solution to the problem.

Prompt

Instead of ServeHTTP I think you mean HandleContext. Is that correct?

Response

Yes, you are correct. HandleContext should be used instead of ServeHTTP. Thank you for bringing this to my attention. Here is the corrected code:

package main

import (
    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/gin-gonic/gin"
    "net/http"
)

func handleRequest(albEvent events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) {
    ginContext, engine := gin.CreateTestContext(nil)
    req, err := http.NewRequest(albEvent.HTTPMethod, albEvent.Path, nil)
    if err != nil {
        return events.ALBTargetGroupResponse{StatusCode: http.StatusBadRequest}, err
    }
    for headerName, headerValues := range albEvent.Headers {
        for _, headerValue := range headerValues {
            req.Header.Add(headerName, headerValue)
        }
    }
    ginContext.Request = req
    engine.HandleContext(ginContext)

    return events.ALBTargetGroupResponse{
        StatusCode: ginContext.Writer.Status(),
        Body:       ginContext.Writer.Body.String(),
        Headers: map[string]string{
            "Content-Type": ginContext.GetHeader("Content-Type"),
        },
    }, nil
}

func main() {
    lambda.Start(handleRequest)
}

Review

Now this is interesting. ChatGPT is trying to bypass HTTP handling and simply go straight into the backend of Gin. I hadn't thought of doing something like this.

Grabbing a Gin context struct is not easily, and constructing one is harder. That's not how Gin wants you to play with its system. So I find it interesting that ChatGPT is using the test harness to create a dummy context, then passing in a request. This is similar to how I was doing it with use httptest.ResponseRecorder in my own tests, before I decided to ask ChatGPT for help.

But… something is still off with this code. I can't put my finger on it, but it feels wrong. The nice thing about this code, however, is that I'm not here to use it verbatim. I'm here to get ideas on where to go next. And in order for me to move forward, I need to understand why ChatGPT chose this code and what it was thinking. So I prompt it for some explanation.

Prompt

What does HandleContext do that differs from ServeHTTP?

Response

HandleContext is a method provided by the Gin Gonic framework that is used to handle incoming HTTP requests in a way that is more convenient for use in a Go context, such as a Lambda function. Unlike ServeHTTP, HandleContext takes a *gin.Context struct as an argument instead of an http.ResponseWriter and *http.Request pair.

HandleContext provides access to a more feature-rich Context struct that contains all of the information from the incoming request, such as headers, query parameters, and request body, as well as the response writer for sending back a response. This allows for more flexibility and control over the incoming request and outgoing response, making it a more suitable choice for handling requests in a Go context.

In comparison, ServeHTTP is a method that is provided by the http.Handler interface, which is used to handle incoming HTTP requests in a more general-purpose way that is suitable for use in a wide range of contexts. While it can be used to handle requests in a Lambda function, it requires manual manipulation of the http.ResponseWriter and *http.Request pair to access request data and send back a response.

Review

Alright. This is a pretty impressive explanation of how these functions differ. And note that ChatGPT isn't reading from StackOverflow or other webpage, it is somehow gathering this information from the code or… something. I can't quite figure it out.

However, I'm all about trust but verify so I want to make sure that this code is legit. I want to see if ChatGPT can help check its own work. Next prompt.

Prompt

Write a unit test for handleRequest that uses table-driven tests.

Response

Here is an example of a unit test for the handleRequest function using table-driven tests in Go:

package main

import (
    "github.com/aws/aws-lambda-go/events"
    "github.com/stretchr/testify/assert"
    "net/http"
    "testing"
)

func TestHandleRequest(t *testing.T) {
    testCases := []struct {
        name           string
        albEvent       events.ALBTargetGroupRequest
        expectedResult events.ALBTargetGroupResponse
        expectedError  error
    }{
        {
            name: "Test Case 1: Valid Request",
            albEvent: events.ALBTargetGroupRequest{
                HTTPMethod: "GET",
                Path:       "/ping",
                Headers: map[string][]string{
                    "Content-Type": {"application/json"},
                },
            },
            expectedResult: events.ALBTargetGroupResponse{
                StatusCode: http.StatusOK,
                Body:       "pong",
                Headers: map[string]string{
                    "Content-Type": "application/json",
                },
            },
            expectedError: nil,
        },
        {
            name: "Test Case 2: Invalid Method",
            albEvent: events.ALBTargetGroupRequest{
                HTTPMethod: "POST",
                Path:       "/ping",
                Headers: map[string][]string{
                    "Content-Type": {"application/json"},
                },
            },
            expectedResult: events.ALBTargetGroupResponse{
                StatusCode: http.StatusMethodNotAllowed,
                Body:       "",
                Headers: map[string]string{
                    "Content-Type": "application/json",
                },
            },
            expectedError: nil,
        },
    }

    for _, testCase := range testCases {
        t.Run(testCase.name, func(t *testing.T) {
            result, err := handleRequest(testCase.albEvent)
            assert.Equal(t, testCase.expectedResult, result)
            assert.Equal(t, testCase.expectedError, err)
        })
    }
}

In this example, we define a slice of structs, each of which represents a single test case. The structs contain fields for the input albEvent, the expected result expectedResult, and the expected error expectedError.

In the test function TestHandleRequest, we use a for loop to iterate over the test cases and call handleRequest with the input albEvent from each test case. Then, we use the assert.Equal method from the stretchr/testify library to compare the actual result and error with the expected result and error. If any of these assertions fail, the test case will fail and the failure will be reported.

Review

Now this is cool. Not only did ChatGPT create a unit test for the handleRequest function, with a correct set of test cases, it also did it in table-driven tests, as requested. I didn't need to explain what a Table-Driven Test was, or how it commonly looks within Go. It just knew. That is wild.

I've now found two really good use cases for ChatGPT:

  1. Helping debug issues in programming.
  2. Helping create boilerplate code.

I love table-driven tests, but I hate the tedium of setting them up. They are mostly all the same, so I could setup a macro to do it, but they differ in just the right annoying ways that I can't easily make a generic macro that is useful. So I end up coding them by hand every time. Now, I can get ChatGPT to create a basic structure for me.

In the next part of this series I'm going to get ChatGPT to stretch its legs and think about the full software development stack.