The ghttp package provides HTTP server testing utilities for verifying HTTP client behavior. It wraps Go's httptest server with a handler queue system and composable verification handlers for expressive HTTP testing.
import "github.com/onsi/gomega/ghttp"The ghttp package enables testing HTTP clients by providing a test server that supports registering multiple handlers. Handlers are processed sequentially - the first request goes to the first handler, the second to the second handler, etc. Each handler verifies the incoming request and provides a response.
The Server type wraps httptest.Server and manages handler queues and request routing.
type Server struct {
HTTPTestServer *httptest.Server // Underlying httptest server
AllowUnhandledRequests bool // Allow requests without handlers
UnhandledRequestStatusCode int // Status code for unhandled requests
Writer io.Writer // Output writer for logs
}// Creates and starts new test HTTP server
func NewServer() *Server
// Creates server without starting it
func NewUnstartedServer() *Server
// Creates and starts new test HTTPS server
func NewTLSServer() *Server// Starts an unstarted server
func (s *Server) Start()
// Closes and cleans up server
func (s *Server) Close()
// Returns server base URL
func (s *Server) URL() string
// Returns server address
func (s *Server) Addr() string
// Implements http.Handler interface
func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request)
// Forcibly closes all client connections
func (s *Server) CloseClientConnections()// Adds handlers to end of queue
func (s *Server) AppendHandlers(handlers ...http.HandlerFunc)
// Sets handler at specific index
func (s *Server) SetHandler(index int, handler http.HandlerFunc)
// Gets handler at specific index
func (s *Server) GetHandler(index int) http.HandlerFunc
// Wraps existing handler with new handler
func (s *Server) WrapHandler(index int, handler http.HandlerFunc)
// Routes method+path to specific handler
func (s *Server) RouteToHandler(method string, path any, handler http.HandlerFunc)
// Resets all handlers and state
func (s *Server) Reset()The RouteToHandler method accepts either a string or *regexp.Regexp for the path parameter.
// Configure unhandled request behavior
func (s *Server) SetAllowUnhandledRequests(bool)
func (s *Server) GetAllowUnhandledRequests() bool
// Set status code for unhandled requests
func (s *Server) SetUnhandledRequestStatusCode(int)
func (s *Server) GetUnhandledRequestStatusCode() int// Returns all received requests (both handled and unhandled)
func (s *Server) ReceivedRequests() []*http.Request// Creates RoundTripper that routes to this server
func (s *Server) RoundTripper(rt http.RoundTripper) http.RoundTripperVerification handlers check that incoming requests match expected criteria. They can be combined using CombineHandlers.
// Combines multiple handlers into one sequential handler
func CombineHandlers(handlers ...http.HandlerFunc) http.HandlerFunc// Verifies HTTP method, path, and optional query string
// path can be string or types.GomegaMatcher
func VerifyRequest(method string, path any, rawQuery ...string) http.HandlerFunc// Verifies Content-Type header matches exactly
func VerifyContentType(contentType string) http.HandlerFunc
// Verifies MIME type (Content-Type without parameters)
func VerifyMimeType(mimeType string) http.HandlerFunc
// Verifies HTTP Basic Authentication credentials
func VerifyBasicAuth(username string, password string) http.HandlerFunc
// Verifies request contains all specified headers
func VerifyHeader(header http.Header) http.HandlerFunc
// Verifies specific header key has specified values
func VerifyHeaderKV(key string, values ...string) http.HandlerFunc
// Verifies Host header (host can be string or matcher)
func VerifyHost(host any) http.HandlerFunc// Verifies request body matches byte array
func VerifyBody(expectedBody []byte) http.HandlerFunc
// Verifies request body is equivalent JSON
func VerifyJSON(expectedJSON string) http.HandlerFunc
// Verifies request body JSON matches marshaled object
func VerifyJSONRepresenting(object any) http.HandlerFunc
// Verifies request body is equivalent Protocol Buffer message
func VerifyProtoRepresenting(expected protoiface.MessageV1) http.HandlerFunc// Verifies form values (url-encoded or multipart)
func VerifyForm(values url.Values) http.HandlerFunc
// Verifies specific form field has specified values
func VerifyFormKV(key string, values ...string) http.HandlerFuncResponse handlers generate HTTP responses for requests.
// Responds with status code and body (string or []byte)
func RespondWith(statusCode int, body any, optionalHeader ...http.Header) http.HandlerFunc
// Responds with pointer to status code (allows runtime modification)
func RespondWithPtr(statusCode *int, body any, optionalHeader ...http.Header) http.HandlerFunc// Responds with JSON-encoded object
func RespondWithJSONEncoded(statusCode int, object any, optionalHeader ...http.Header) http.HandlerFunc
// JSON response with pointer to status code
func RespondWithJSONEncodedPtr(statusCode *int, object any, optionalHeader ...http.Header) http.HandlerFunc// Responds with Protocol Buffer message
func RespondWithProto(statusCode int, message protoadapt.MessageV1, optionalHeader ...http.Header) http.HandlerFuncThe GHTTPWithGomega type allows creating handlers bound to a specific Gomega instance, useful for testing in isolation or with custom failure handlers.
type GHTTPWithGomega struct {
// Factory for handlers bound to specific Gomega instance
}
// Creates handler factory with custom Gomega instance
func NewGHTTPWithGomega(gomega Gomega) *GHTTPWithGomegaAll verification and response handler functions are available as methods on GHTTPWithGomega:
func (g GHTTPWithGomega) VerifyRequest(method string, path any, rawQuery ...string) http.HandlerFunc
func (g GHTTPWithGomega) VerifyContentType(contentType string) http.HandlerFunc
func (g GHTTPWithGomega) VerifyMimeType(mimeType string) http.HandlerFunc
func (g GHTTPWithGomega) VerifyBasicAuth(username string, password string) http.HandlerFunc
func (g GHTTPWithGomega) VerifyHeader(header http.Header) http.HandlerFunc
func (g GHTTPWithGomega) VerifyHeaderKV(key string, values ...string) http.HandlerFunc
func (g GHTTPWithGomega) VerifyHost(host any) http.HandlerFunc
func (g GHTTPWithGomega) VerifyBody(expectedBody []byte) http.HandlerFunc
func (g GHTTPWithGomega) VerifyJSON(expectedJSON string) http.HandlerFunc
func (g GHTTPWithGomega) VerifyJSONRepresenting(object any) http.HandlerFunc
func (g GHTTPWithGomega) VerifyForm(values url.Values) http.HandlerFunc
func (g GHTTPWithGomega) VerifyFormKV(key string, values ...string) http.HandlerFunc
func (g GHTTPWithGomega) VerifyProtoRepresenting(expected protoiface.MessageV1) http.HandlerFunc
func (g GHTTPWithGomega) RespondWith(statusCode int, body any, optionalHeader ...http.Header) http.HandlerFunc
func (g GHTTPWithGomega) RespondWithPtr(statusCode *int, body any, optionalHeader ...http.Header) http.HandlerFunc
func (g GHTTPWithGomega) RespondWithJSONEncoded(statusCode int, object any, optionalHeader ...http.Header) http.HandlerFunc
func (g GHTTPWithGomega) RespondWithJSONEncodedPtr(statusCode *int, object any, optionalHeader ...http.Header) http.HandlerFunc
func (g GHTTPWithGomega) RespondWithProto(statusCode int, message protoadapt.MessageV1, optionalHeader ...http.Header) http.HandlerFuncFunction type implementing http.RoundTripper interface:
type RoundTripperFunc func(*http.Request) (*http.Response, error)
func (fn RoundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error)import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/ghttp"
)
var _ = Describe("HTTP Client", func() {
var server *ghttp.Server
var client *MyHTTPClient
BeforeEach(func() {
server = ghttp.NewServer()
client = NewMyHTTPClient(server.URL())
})
AfterEach(func() {
server.Close()
})
It("should make GET request", func() {
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", "/api/users"),
ghttp.RespondWith(200, `{"users": []}`),
),
)
users, err := client.GetUsers()
Expect(err).NotTo(HaveOccurred())
Expect(users).To(BeEmpty())
})
})It("should send basic auth credentials", func() {
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", "/protected"),
ghttp.VerifyBasicAuth("username", "password"),
ghttp.RespondWith(200, "success"),
),
)
response, err := client.GetProtectedResource()
Expect(err).NotTo(HaveOccurred())
Expect(response).To(Equal("success"))
})It("should send JSON payload", func() {
expectedUser := User{Name: "Alice", Email: "alice@example.com"}
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("POST", "/api/users"),
ghttp.VerifyJSONRepresenting(expectedUser),
ghttp.RespondWithJSONEncoded(201, expectedUser),
),
)
created, err := client.CreateUser("Alice", "alice@example.com")
Expect(err).NotTo(HaveOccurred())
Expect(created).To(Equal(expectedUser))
})It("should send custom headers", func() {
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", "/api/data"),
ghttp.VerifyHeader(http.Header{
"X-API-Key": []string{"secret-key"},
"Accept": []string{"application/json"},
}),
ghttp.RespondWith(200, `{"data": "value"}`),
),
)
data, err := client.GetData()
Expect(err).NotTo(HaveOccurred())
})Describe("API endpoint", func() {
var statusCode int
var response string
BeforeEach(func() {
statusCode = 200
response = `{"status": "ok"}`
server.AppendHandlers(
ghttp.RespondWithPtr(&statusCode, &response),
)
})
Context("when server is healthy", func() {
It("should return 200", func() {
result, err := client.HealthCheck()
Expect(err).NotTo(HaveOccurred())
Expect(result.Status).To(Equal("ok"))
})
})
Context("when server is unhealthy", func() {
BeforeEach(func() {
statusCode = 503
response = `{"status": "error"}`
})
It("should return 503", func() {
_, err := client.HealthCheck()
Expect(err).To(HaveOccurred())
})
})
})BeforeEach(func() {
// Route always handles GET /health regardless of handler queue
server.RouteToHandler("GET", "/health",
ghttp.RespondWith(200, `{"status": "ok"}`),
)
// Route with regex pattern
server.RouteToHandler("GET", regexp.MustCompile(`/api/users/\d+`),
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", MatchRegexp(`/api/users/\d+`)),
ghttp.RespondWith(200, `{"user": {}}`),
),
)
})It("should augment existing handler", func() {
server.AppendHandlers(
ghttp.RespondWith(200, "original response"),
)
// Add additional verification to existing handler
server.WrapHandler(0, ghttp.VerifyHeaderKV("X-Custom", "value"))
// Request must now pass both handlers
response := client.MakeRequest()
Expect(response).To(Equal("original response"))
})It("should submit form data", func() {
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("POST", "/submit"),
ghttp.VerifyFormKV("username", "alice"),
ghttp.VerifyFormKV("password", "secret"),
ghttp.RespondWith(200, "success"),
),
)
err := client.Login("alice", "secret")
Expect(err).NotTo(HaveOccurred())
})It("should make multiple requests", func() {
server.AppendHandlers(
ghttp.RespondWith(200, "response1"),
ghttp.RespondWith(200, "response2"),
)
client.Request1()
client.Request2()
requests := server.ReceivedRequests()
Expect(requests).To(HaveLen(2))
Expect(requests[0].URL.Path).To(Equal("/path1"))
Expect(requests[1].URL.Path).To(Equal("/path2"))
})func TestHTTPClient(t *testing.T) {
g := NewWithT(t)
ghttp := ghttp.NewGHTTPWithGomega(g)
server := ghttp.NewServer()
defer server.Close()
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", "/test"),
ghttp.RespondWith(200, "ok"),
),
)
// Test with standard testing.T
}BeforeEach(func() {
server.SetAllowUnhandledRequests(true)
server.SetUnhandledRequestStatusCode(404)
})
It("should handle unexpected requests gracefully", func() {
// No handlers registered, but won't fail
response, err := client.MakeUnexpectedRequest()
Expect(err).To(HaveOccurred())
// Server returned 404 for unhandled request
})The server processes requests in the following order:
RouteToHandler, that handler is calledAppendHandlers, those handlers are called in orderAllowUnhandledRequests is true, the request receives UnhandledRequestStatusCodeAllowUnhandledRequests is false, the test fails with an errorCompose verification and response handlers for readability:
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("POST", "/api/resource"),
ghttp.VerifyJSON(`{"name": "test"}`),
ghttp.VerifyHeader(http.Header{"Authorization": []string{"Bearer token"}}),
ghttp.RespondWithJSONEncoded(201, createdResource),
),
)Share handler setup across contexts while allowing variation:
var statusCode int
var response MyResponse
BeforeEach(func() {
statusCode = 200
response = MyResponse{Status: "ok"}
server.AppendHandlers(
ghttp.RespondWithJSONEncodedPtr(&statusCode, &response),
)
})
// Nested contexts can modify statusCode and responseFor endpoints that should always respond the same way (like health checks):
BeforeEach(func() {
server.RouteToHandler("GET", "/health",
ghttp.RespondWith(200, "ok"),
)
})Use ReceivedRequests() when you need to verify request order or content after the fact:
It("should make requests in order", func() {
// Set up multiple handlers
server.AppendHandlers(
ghttp.RespondWith(200, "1"),
ghttp.RespondWith(200, "2"),
)
client.BatchOperation()
requests := server.ReceivedRequests()
Expect(requests).To(HaveLen(2))
Expect(requests[0].URL.Path).To(Equal("/first"))
Expect(requests[1].URL.Path).To(Equal("/second"))
})Always close the server to free resources:
AfterEach(func() {
server.Close()
})Verification handlers that accept matchers (like VerifyRequest and VerifyHost) can use any Gomega matcher:
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", ContainSubstring("/api/users")),
ghttp.VerifyRequest("GET", MatchRegexp(`/api/users/\d+`)),
ghttp.VerifyHost(HavePrefix("api.")),
ghttp.RespondWith(200, "ok"),
),
)This allows flexible matching beyond exact string equality.