go-http
Skill7 starsChanged 9 days agoApache-2.0View on GitHub
Use when writing or reviewing Go HTTP code — handlers, routing with net/http ServeMux, middleware, request decoding and response encoding, server timeouts and graceful shutdown, or HTTP clients. Also use when building a REST or JSON API endpoint or calling an external HTTP service, even if the user names a framework instead of net/http. Does not cover the test server helpers (see go-testing) or request-scoped logging (see go-logging).
1 other project uses a version of this.
Adopted by
Most recently taken up by
- Dankosik/golang-backend-skillstheir own version, first seen Sep 8, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- Dankosik/golang-backend-skills★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository25 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- go-code-refactorSkill
- go-code-reviewSkill
- go-codeSkill
- go-concurrencySkill
- go-contextSkill
- go-databaseSkill
- go-data-structuresSkill
- go-defensiveSkill
- go-documentationSkill
- go-error-handlingSkill
- go-functionsSkill
- go-genericsSkill
- go-interfacesSkill
- go-lintingSkill
- go-loggingSkill
- go-namingSkill
- go-packagesSkill
- go-performanceSkill
- go-resilienceSkill
- go-securitySkill
- go-style-coreSkill
- go-testingSkill
- go-troubleshootingSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 50
Only when the contract makes HEAD a 405, one package function builds the answer, registered twice per GET path — as HEAD and without a …
- Very specific: line 204
Never http.Get, http.Post, or http.DefaultClient — no timeout.
- Very specific: line 240
Check resp.StatusCode before decoding.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Go HTTP Servers and Clients
Compatibility: Baseline Go 1.27 (see COMPATIBILITY.md).
ServeMuxmethod and wildcard patterns require Go 1.22+;http.NewCrossOriginProtectionGo 1.25+;http.Server.MaxHeaderValueCountandencoding/json/v2Go 1.27+.
Resource Routing
references/WEB-SERVER.md- Read when assembling a complete server: routing, handler, and graceful shutdown in onemain.references/JSON-V2.md- Read when choosing JSON v2 I/O APIs, decoding a bounded document, migrating wire formats, or testing exact JSON bytes (Go 1.27+).references/GRPC.md- Read when writing a gRPC server or client: status codes, generated-server embedding,grpc.NewClient, boundedGracefulStop, and bufconn tests.
Routing (Go 1.22+)
A package already on a framework keeps it, and the rules below still apply; go-packages owns adding a router module.
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", s.handleGetUser) // serves GET and HEAD
mux.HandleFunc("GET /users", s.handleListUsers)
mux.HandleFunc("POST /users", s.handleCreateUser)
mux.HandleFunc("GET /{$}", s.handleIndex) // exact "/", not a subtree
id := r.PathValue("id")- A
GETpattern also servesHEAD. A method pattern answers every other method on its path with 405 and anAllowheader that listsHEAD;HEADis the one it lets through, with theGEThandler's status. Under a contract that makes every method butGETa 405, everyGETpath gets aHEADpattern and a pattern without a method, both answered with the methods that path serves — the health check and the index as much as the resource routes, so the file registers as manyHEADpatterns asGETpatterns — or aGET-only path registers its handler without a method and opens it with anr.Method != http.MethodGetcheck that answers 405 withAllow: GET; the test file sendsHEADandPOSTto eachGETpath and readsAllow. A pattern without a method matches every method. - Conflicting patterns panic at registration; overlapping patterns are valid
when one is more specific, which is why
HEAD /users/{id}registers besideGET /users/{id}. - Trailing
/is a subtree;{$}pins the exact path.
Only when the contract makes HEAD a 405, one package function builds the
answer, registered twice per GET path — as HEAD and without a method, so
POST and DELETE get the same Allow — the index included:
// methodNotAllowed answers every method its path does not serve, HEAD among
// them; allow lists the methods the path does serve.
func methodNotAllowed(allow string) http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Allow", allow)
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
mux.HandleFunc("HEAD /users/{id}", methodNotAllowed("GET"))
mux.HandleFunc("/users/{id}", methodNotAllowed("GET"))
mux.HandleFunc("HEAD /users", methodNotAllowed("GET, POST"))
mux.HandleFunc("/users", methodNotAllowed("GET, POST"))
mux.HandleFunc("HEAD /{$}", methodNotAllowed("GET"))
mux.HandleFunc("/{$}", methodNotAllowed("GET"))Handler Shape
Preserve the existing handler structure. No package-level state.
Bound and decode → validate → call the domain with r.Context() → map the
error → write once. Server below stands for the type the repository already
has; in new code without one, the handler is a function literal in the
constructor that builds the mux, capturing the store
(go-code). With
import json "encoding/json/v2" (Go 1.27):
func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
var req struct {
Name string `json:"name"`
}
// One decode: an unknown member, a second value, junk, or an oversized body fails.
if err := json.UnmarshalRead(r.Body, &req, json.RejectUnknownMembers(true)); err != nil {
http.Error(w, "invalid JSON body", http.StatusBadRequest)
return
}
if req.Name == "" {
http.Error(w, "name is required", http.StatusBadRequest)
return
}
user, err := s.store.Create(r.Context(), req.Name)
switch {
case errors.Is(err, ErrConflict):
http.Error(w, "name already taken", http.StatusConflict)
return
case err != nil && errors.Is(r.Context().Err(), context.Canceled):
slog.DebugContext(r.Context(), "create user: client gone", "err", err)
return // nobody is left to read a status
case err != nil:
slog.ErrorContext(r.Context(), "create user", "err", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
body, err := json.Marshal(struct { // the response names its fields; a stored one added later stays out
ID string `json:"id"`
Name string `json:"name"`
}{user.ID, user.Name})
if err != nil { // before any header, so an encode error is still a 500
slog.ErrorContext(r.Context(), "encode user", "err", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_, _ = w.Write(body) // headers are sent; a failed write is the client's disconnect
}- Bound every decoded body with
http.MaxBytesReader.{}andnullstill decode, leavingreqzero, so required fields are validated before the store call. The request type is declared in the handler that reads it; a second handler decoding the same document is what moves it to package level (go-code). - A package already decoding with
encoding/jsonv1 keeps v1 and its two decodes:dec.DisallowUnknownFields(),dec.Decode(&req), thendec.Decode(new(any))must returnio.EOF—DisallowUnknownFieldsalone accepts trailing data, and the second decode allows trailing whitespace while the cap still holds. - v2 encodes a nil slice as
[]and a nil map as{}, so a filtered list with no matches is written as it is, with nomake([]T, 0, n); that line belongs to a package on v1, or underFormatNilSliceAsNull(true)(JSON-V2.md). - A response body is marshalled with
json.Marshalbefore the first header, as above. v2 fails at encode time on atime.Durationfield (no default representation), invalid UTF-8 in stored text, or aMarshalJSONerror;json.MarshalWrite(w, v)afterWriteHeadersends the status with a truncated body and nowhere to report the error. - A write whose error has nowhere to go is discarded in the open with its
reason on the line, as the example's last line is, never bare.
errcheckreads a barew.Write,io.WriteString(w, …),json.MarshalWrite(w, v), orjson.NewEncoder(w).Encode(v)as a finding, and//nolint:errcheckis not a way out:gosecG104 reports the same line.
Mapping errors to status codes
Centralize shared error-to-status rules in writeError, using
errors.Is/errors.AsType; keep a single-handler mapping local. Apply each
row where that failure can occur:
| Error | Status | Body |
| Validation, malformed input | 400 | The validation message |
ErrNotFound sentinel | 404 | Generic |
ErrConflict, version mismatch | 409 | Generic |
Incoming r.Context().Err() is context.Canceled | 499-style: log at Debug, write nothing | — |
Downstream context.Canceled while the incoming request is alive | 500 unless the endpoint specifies otherwise | Generic |
context.DeadlineExceeded from downstream | 504 | Generic |
| Anything else | 500 | Generic — never err.Error() |
The request ID reaches the 500 record only through the request-scoped
logger go-logging sets up;
where the repository has one, the example's slog calls go through it. This is
the handle-once exception owned by
go-error-handling.
Middleware
func(next http.Handler) http.Handler. Outside in: recover → request ID +
logging → auth → mux. Wrap http.ResponseWriter only to capture status and
keep it transparent; use http.NewResponseController(w) for deadlines and
flushing instead of asserting optional interfaces.
Server Construction
Construct an http.Server; bare http.ListenAndServe sets no timeouts.
| Field | Why |
ReadHeaderTimeout | Slowloris defense; must never be zero |
ReadTimeout, WriteTimeout | Bound slow clients; WriteTimeout exceeds the slowest handler |
IdleTimeout | Reclaim keep-alive connections |
MaxHeaderBytes, MaxHeaderValueCount (Go 1.27+) | Only to change the defaults, 1 MiB and 500 values: MaxHeaderBytes: 1 << 20 restates zero |
Handler: http.NewCrossOriginProtection().Handler(mux) | CSRF for state-changing requests (Go 1.25+). One per server: with CORS it is the instance go-security's withCORS builds with the trusted origins; a second, empty one around it answers every cross-origin POST with 403 |
BaseContext | Never the signal.NotifyContext context when in-flight requests should drain: it cancels every request at the signal, before Shutdown waits for them. Handlers that must see shutdown get a separate context, cancelled once Shutdown's timeout expires |
One slow route does not raise WriteTimeout for all: wrap it in
http.TimeoutHandler(h, d, msg), which cancels r.Context() at d and
answers 503. It buffers the response and has no Flusher, so a streaming
route sets its own deadline with http.NewResponseController(w) instead.
For graceful shutdown, signal.NotifyContext owns the lifetime;
ListenAndServe runs in one goroutine feeding a buffered error channel;
srv.Shutdown(ctx) gets its own timeout.
Clients
Never
http.Get,http.Post, orhttp.DefaultClient— no timeout. One tuned*http.Transportand one*http.Client{Timeout: d, Transport: t}per dependency, built once and reused. The Transport owns the connection pool: aClientwith a nilTransportshareshttp.DefaultTransport, which keeps two idle connections per host (http.DefaultMaxIdleConnsPerHost). Tune a clone of it; a&http.Transport{...}literal dropsProxyFromEnvironmentand the dial, TLS-handshake, and idle-connection timeouts (the SSRF dialer in go-security drops the proxy on purpose), and withDialContextorTLSClientConfigset it speaks HTTP/1.1 only unlessForceAttemptHTTP2is true — anHTTP2config on it is ignored. A client that keeps HTTP/2 connections open — watch streams, many multiplexed requests to one host — setsSendPingTimeout(Go 1.24+): at zero no health check runs, and every stream on a connection a NAT dropped silently hangs until its own deadline:t := http.DefaultTransport.(*http.Transport).Clone() t.MaxIdleConnsPerHost = 32 t.HTTP2 = &http.HTTP2Config{SendPingTimeout: 30 * time.Second} client := &http.Client{Timeout: 10 * time.Second, Transport: t}http.NewRequestWithContext(ctx, ...)— the ctx-less form is unbounded;noctxin the lint gate flags it.- The Transport reads the request body on its own goroutine, and that read
can outlive
Do: a 4xx/5xx answered from the headers, a timeout, or a cancel returns mid-upload, and on HTTP/2 the Transport's ownCloseof the body can overlap aRead. Each request, retry included, gets its ownbytes.NewReaderover bytes nobody writes again — never the previous attempt's reader rewound, never a shared or pooled buffer refilled;-racereports that write against the Transport's body writer. defer resp.Body.Close()right after theerr != nilreturn, on every status (bodycloseflags a miss); on errorDoreturns no body to close. Bound untrusted bodies and close them on every path. Go 1.27 drains an unread HTTP/1 body onClose— up to 256 KiB and 50 ms — to keep the connection reusable, so closing is the whole obligation; a manual read to EOF only buys reuse for responses past those bounds.- Check
resp.StatusCodebefore decoding; a 5xx body is not your struct. - For retry eligibility,
Retry-After, replay safety, and coordinated budgets, use go-resilience. HTTP status alone does not establish whether repeating an operation is safe.
Bounded Response Bodies
For a strict response-size limit, read at most the limit plus one byte,
reject an oversized result, then decode the complete buffer into dst.
A decoder can finish its first object before io.LimitReader reaches its
limit; that alone does not enforce the size of the whole response.
const maxBody = 64 << 10
body, err := io.ReadAll(io.LimitReader(resp.Body, maxBody+1))
if err != nil {
return fmt.Errorf("read response: %w", err)
}
if len(body) > maxBody {
return fmt.Errorf("response exceeds %d bytes", maxBody)
}
return json.Unmarshal(body, dst)Partial Updates
A PATCH body names only the members it changes, and a Go struct cannot tell a
missing member from a zero one: decoding into a fresh struct and saving it
zeroes every field the client left out. Load the record, copy its
client-writable fields into the request value, and unmarshal the body onto
that value. v2 leaves an absent member as loaded and sets one sent as null
to its zero value (v1 leaves it unchanged). RejectUnknownMembers(true)
refuses password_hash and any member the request type does not hold, and a
top-level null leaves the pointer nil. v2 decodes into what the request
value already holds — it writes a non-nil slice's array, merges into a
non-nil map, decodes through a non-nil pointer — so a slice, map, or pointer
field is copied with slices.Clone, maps.Clone, or a new value; copied as
is, the record changes before the body is rejected:
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
req := &struct {
Email string `json:"email"`
Phone string `json:"phone"` // "phone": null clears it
Tags []string `json:"tags"`
}{acct.Email, acct.Phone, slices.Clone(acct.Tags)} // never acct.Tags: v2 writes into its array
if err := json.UnmarshalRead(r.Body, &req, json.RejectUnknownMembers(true)); err != nil || req == nil {
http.Error(w, "invalid JSON body", http.StatusBadRequest)
return
}
if req.Email == "" { // "email": null arrives as ""
http.Error(w, "email is required", http.StatusBadRequest)
return
}
acct.Email, acct.Phone, acct.Tags = req.Email, req.Phone, req.Tags // a rejected body never touched acctRelated Skills
- go-context: derived deadlines and request lifetime.
- go-database: handler queries and transactions.
- go-testing:
httptestandsynctest. - go-concurrency: server goroutine and shared state.
- go-security: input naming files, URLs or commands; cookies, SSRF, and error disclosure.
- go-data-structures: nil versus empty slices and maps before they reach the encoder.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

