--- name: fp-go-http description: >- Use this skill when making HTTP requests in fp-go using the ReaderIOResult-based HTTP client (github.com/IBM/fp-go/v2/context/readerioresult/http). Trigger on mentions of fp-go HTTP, MakeClient, MakeGetRequest, MakeRequest, ReadJSON, ReadText, ReadAll, ReadFullResponse, the HTTP request builder (WithURL, WithJSON, WithBearer, WithHeader, WithQueryArg), HTTP header name constants such as ContentType, Accept, Authorization or XRequestID from http/headers or content type / media type constants such as JSON, ProblemJSON, FormEncoded or OctetStream from http/content, parallel requests with TraverseArray or TraverseTuple2, or building context-aware, composable HTTP pipelines that propagate errors through the Result monad. --- # fp-go HTTP Requests ## Overview fp-go wraps `net/http` in the `ReaderIOResult` monad, giving you composable, context-aware HTTP operations with automatic error propagation. The core package is: ``` github.com/IBM/fp-go/v2/context/readerioresult/http ``` All HTTP operations are lazy — they describe what to do but do not execute until you call the resulting function with a `context.Context`. **Executing a pipeline.** `ReaderIOResult[A]` is `func(context.Context) func() Result[A]`, so running it takes two calls and yields **one** value: ```go res := pipeline(ctx)() // Result[A] — a single value, NOT (A, error) value, err := R.Unwrap(res) // R = github.com/IBM/fp-go/v2/result ``` Examples below use the shorthand `value, err := R.Unwrap(pipeline(ctx)())`. Writing `value, err := pipeline(ctx)()` is a compile error ("2 variables but … returns 1 value"). ## Before You Generate fp-go is low-frequency in training data, so signatures are easy to misremember. For any combinator not shown below, look it up via the fp-go MCP server's `search_examples` / `get_example` tools (see the **fp-go-mcp** skill) instead of guessing. After writing code, run `go build ./...` and `go vet ./...` and fix any type-parameter or argument-order errors before presenting it. ## Core Types ```go // Requester builds an *http.Request given a context. type Requester = ReaderIOResult[*http.Request] // func(context.Context) func() result.Result[*http.Request] // Client executes a Requester and returns the response wrapped in ReaderIOResult. type Client interface { Do(Requester) ReaderIOResult[*http.Response] } ``` ## Basic Usage ### 1. Create a Client ```go import ( "net/http" H "github.com/IBM/fp-go/v2/context/readerioresult/http" ) client := H.MakeClient(http.DefaultClient) // Or with a custom client: custom := &http.Client{Timeout: 10 * time.Second} client := H.MakeClient(custom) ``` ### 2. Build a Request ```go // GET request (most common) req := H.MakeGetRequest("https://api.example.com/users/1") // Arbitrary method + body req := H.MakeRequest("POST", "https://api.example.com/users", bodyReader) ``` ### 3. Execute and Parse ```go import ( "context" H "github.com/IBM/fp-go/v2/context/readerioresult/http" ) type User struct { ID int `json:"id"` Name string `json:"name"` } client := H.MakeClient(http.DefaultClient) // ReadJSON validates status, Content-Type, then unmarshals JSON result := H.ReadJSON[User](client)(H.MakeGetRequest("https://api.example.com/users/1")) // Execute — provide context once. The inner () yields a Result[User] (one value). user, err := R.Unwrap(result(context.Background())()) ``` ### 4. Timeouts and Request-Scoped Values `http.Client.Timeout` is a global cap. For a per-request or per-pipeline bound, scope the context instead: requests honour context cancellation, and the cancel func is released automatically. ```go bounded := F.Pipe2( H.ReadJSON[User](client)(H.MakeGetRequest("https://api.example.com/users/1")), RIO.WithTimeout[User](2*time.Second), RIO.WithValue[User](traceIDKey, traceID), // read downstream with RIO.AskValue[string](traceIDKey) ) ``` Never write `ctx, cancel := context.WithTimeout(...)` around the call or `ctx.Value(key).(V)` inside it. See the `fp-go-context` skill for the full context-handling guide. ## Response Readers All accept a `Client` and return a function `Requester → ReaderIOResult[A]`: | Function | Returns | Notes | |----------|---------|-------| | `ReadJSON[A](client)` | `ReaderIOResult[A]` | Validates status + Content-Type, unmarshals JSON | | `ReadText(client)` | `ReaderIOResult[string]` | Validates status, reads body as UTF-8 string | | `ReadAll(client)` | `ReaderIOResult[[]byte]` | Validates status, returns raw body bytes | | `ReadFullResponse(client)` | `ReaderIOResult[FullResponse]` | Returns `Pair[*http.Response, []byte]` | `FullResponse = Pair[*http.Response, []byte]` — use `pair.First` / `pair.Second` to access components. ## Composing Requests in Pipelines ```go import ( F "github.com/IBM/fp-go/v2/function" H "github.com/IBM/fp-go/v2/context/readerioresult/http" RIO "github.com/IBM/fp-go/v2/context/readerioresult" IO "github.com/IBM/fp-go/v2/io" ) client := H.MakeClient(http.DefaultClient) readPost := H.ReadJSON[Post](client) pipeline := F.Pipe2( H.MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1"), readPost, RIO.TapIOK(IO.Logf[Post]("Got post: %v")), ) post, err := R.Unwrap(pipeline(context.Background())()) ``` ## Parallel Requests — Homogeneous Types Use `RIO.TraverseArray` when all requests return the same type: ```go import ( A "github.com/IBM/fp-go/v2/array" F "github.com/IBM/fp-go/v2/function" H "github.com/IBM/fp-go/v2/context/readerioresult/http" RIO "github.com/IBM/fp-go/v2/context/readerioresult" IO "github.com/IBM/fp-go/v2/io" N "github.com/IBM/fp-go/v2/number" S "github.com/IBM/fp-go/v2/string" ) type PostItem struct { UserID uint `json:"userId"` ID uint `json:"id"` Title string `json:"title"` } client := H.MakeClient(http.DefaultClient) readPost := H.ReadJSON[PostItem](client) // index -> URL, point-free postURL := F.Flow2(N.Add(1), S.Format[int]("https://jsonplaceholder.typicode.com/posts/%d")) // Fetch 10 posts in parallel data := F.Pipe3( A.MakeBy(10, postURL), RIO.TraverseArray(F.Flow3( H.MakeGetRequest, readPost, RIO.TapIOK(IO.Logf[PostItem]("Post: %v")), )), RIO.TapIOK(IO.Logf[[]PostItem]("All posts: %v")), RIO.Map(A.Size[PostItem]), ) count, err := R.Unwrap(data(context.Background())()) ``` ## Parallel Requests — Heterogeneous Types Use `RIO.TraverseTuple2` (or `Tuple3`, etc.) when requests return different types: ```go import ( T "github.com/IBM/fp-go/v2/tuple" RIO "github.com/IBM/fp-go/v2/context/readerioresult" H "github.com/IBM/fp-go/v2/context/readerioresult/http" F "github.com/IBM/fp-go/v2/function" ) type CatFact struct { Fact string `json:"fact"` } client := H.MakeClient(http.DefaultClient) readPost := H.ReadJSON[PostItem](client) readCatFact := H.ReadJSON[CatFact](client) // Execute both requests in parallel with different response types data := F.Pipe3( T.MakeTuple2( "https://jsonplaceholder.typicode.com/posts/1", "https://catfact.ninja/fact", ), T.Map2(H.MakeGetRequest, H.MakeGetRequest), // build both requesters RIO.TraverseTuple2(readPost, readCatFact), // run in parallel, typed RIO.TapIOK(IO.Logf[T.Tuple2[PostItem, CatFact]]("Result: %v")), ) both, err := R.Unwrap(data(context.Background())()) // both.F1 is PostItem, both.F2 is CatFact ``` ## Building Requests with the Builder API For complex requests (custom headers, query params, JSON body), use the builder: ```go import ( B "github.com/IBM/fp-go/v2/http/builder" RB "github.com/IBM/fp-go/v2/context/readerioresult/http/builder" F "github.com/IBM/fp-go/v2/function" HD "github.com/IBM/fp-go/v2/http/headers" C "github.com/IBM/fp-go/v2/http/content" ) // GET with query parameters req := F.Pipe2( B.Default, B.WithURL("https://api.example.com/items?page=1"), B.WithQueryArg("limit")("50"), ) requester := RB.Requester(req) // POST with JSON body req := F.Pipe3( B.Default, B.WithURL("https://api.example.com/users"), B.WithPost, B.WithJSON(map[string]string{"name": "Alice"}), // sets Content-Type: application/json (C.JSON) automatically ) requester := RB.Requester(req) // With authentication and custom headers — use the constants, not string literals req := F.Pipe4( B.Default, B.WithURL("https://api.example.com/protected"), B.WithBearer("my-token"), // sets Authorization: Bearer my-token B.WithHeader(HD.Accept)(C.JSON), B.WithHeader(HD.XRequestID)("123"), ) requester := RB.Requester(req) // Execute result := H.ReadJSON[Response](client)(requester) data, err := R.Unwrap(result(ctx)()) ``` ### Builder Functions | Function | Effect | |----------|--------| | `B.WithURL(url)` | Set the target URL | | `B.WithMethod(method)` | Set HTTP method (GET, POST, PUT, DELETE, …) | | `B.WithJSON(v)` | Marshal `v` as JSON body, set `Content-Type: application/json` | | `B.WithBytes(data)` | Set raw bytes body, set `Content-Length` automatically | | `B.WithHeader(key)(value)` | Add a request header | | `B.WithBearer(token)` | Set `Authorization: Bearer ` | | `B.WithQueryArg(key)(value)` | Append a query parameter | | `B.WithGet` / `B.WithPost` / `B.WithPut` / `B.WithDelete` | Pre-bound `WithMethod` for the common verbs | | `B.WithContentType(ct)` | Set the `Content-Type` header | | `B.WithFormData(values)` | `url.Values` body + `Content-Type: application/x-www-form-urlencoded` | | `B.WithoutBody` | Remove any body previously set | | `B.WithoutHeader(key)` / `B.WithoutQueryArg(key)` | Remove a header / query parameter | Every `With*` is an `Endomorphism[*Builder]`, so they chain freely inside `F.PipeN(B.Default, …)`. ## Header Names and Content Types Never spell header names or media types as string literals — fp-go ships constants for both. ### Header Names — `http/headers` (Alias `HD`) `github.com/IBM/fp-go/v2/http/headers` defines the commonly used header names as **lower-case** constants (the form mandated by HTTP/2 and HTTP/3): | Group | Constants | |-------|-----------| | Representation | `ContentType`, `ContentLength`, `ContentEncoding`, `ContentLanguage`, `ContentDisposition`, `ContentRange`, `ContentLocation`, `TransferEncoding`, `Date`, `Link` | | Request | `Accept`, `AcceptCharset`, `AcceptEncoding`, `AcceptLanguage`, `Authorization`, `ProxyAuthorization`, `Cookie`, `Host`, `UserAgent`, `Referer`, `Origin`, `Range`, `Expect`, `Forwarded` | | Response | `Location`, `Server`, `SetCookie`, `WWWAuthenticate`, `ProxyAuthenticate`, `RetryAfter`, `Allow`, `AcceptRanges` | | Caching / conditional | `CacheControl`, `ETag`, `LastModified`, `Expires`, `Age`, `Vary`, `Pragma`, `IfMatch`, `IfNoneMatch`, `IfModifiedSince`, `IfUnmodifiedSince`, `IfRange` | | CORS | `AccessControlAllowOrigin`, `AccessControlAllowMethods`, `AccessControlAllowHeaders`, `AccessControlAllowCredentials`, `AccessControlExposeHeaders`, `AccessControlMaxAge`, `AccessControlRequestMethod`, `AccessControlRequestHeaders` | | Security | `StrictTransportSecurity`, `ContentSecurityPolicy`, `XContentTypeOptions`, `XFrameOptions`, `ReferrerPolicy` | | Proxy / tracing | `XForwardedFor`, `XForwardedHost`, `XForwardedProto`, `XRequestID`, `XCorrelationID`, `Traceparent`, `Tracestate` | Lower case is safe with HTTP/1.1 too: `http.Header.Get/Set/Add/Values/Del` and the builder canonicalize keys. **Only raw map indexing does not** — `h[HD.ContentType]` misses `"Content-Type"`. Use `h.Get(HD.ContentType)` or the lenses below. The package also provides functional access to `http.Header`: ```go HD.AtValue(HD.Authorization).Get(h) // Option[string] — first value HD.AtValues(HD.Accept).Get(h) // Option[[]string] — all values, None if absent h2 := HD.AtValue(HD.ContentType).Set(O.Some(C.JSON))(h) // new header map; O.None removes the header merged := HD.Monoid.Concat(defaults, overrides) // union; values of shared keys are concatenated ``` Both lenses canonicalize the header name, and `Set` returns a new `http.Header`, leaving `h` untouched. ### Content Types — `http/content` (Alias `C`) `github.com/IBM/fp-go/v2/http/content` defines media type constants (bare type, no parameters): | Group | Constants | |-------|-----------| | JSON family | `JSON`, `ProblemJSON`, `JSONPatch`, `MergePatch`, `NDJSON`, `JSONLD`, `HALJSON`, `JSONAPI` | | XML family | `XML`, `TextXML`, `ProblemXML`, `SOAP` | | Other structured | `YAML`, `CBOR`, `Protobuf`, `GRPC` | | Text | `TextPlain`, `TextHTML`, `TextCSS`, `TextCSV`, `TextJavaScript`, `TextMarkdown`, `TextEventStream` | | Forms / multipart | `FormEncoded`, `MultipartFormData`, `MultipartMixed`, `MultipartByteRanges` | | Binary | `OctetStream`, `PDF`, `ZIP`, `Gzip`, `WASM` | | Media | `ImagePNG`, `ImageJPEG`, `ImageGIF`, `ImageWebP`, `ImageAVIF`, `ImageSVG`, `AudioMPEG`, `AudioOGG`, `VideoMP4`, `VideoWebM`, `FontWOFF2` | `C.Json` is deprecated — use `C.JSON`. The builder already uses these: `B.WithJSON` sets `C.JSON`, `B.WithFormData` sets `C.FormEncoded`; `B.WithContentType(ct)` and `B.WithAuthorization(v)` are `B.WithHeader(HD.ContentType)` / `B.WithHeader(HD.Authorization)`. A received `Content-Type` usually carries parameters (`application/json; charset=utf-8`), so parse before comparing against a constant: ```go import ( FH "github.com/IBM/fp-go/v2/http" PA "github.com/IBM/fp-go/v2/pair" R "github.com/IBM/fp-go/v2/result" S "github.com/IBM/fp-go/v2/string" ) isJSON := F.Flow3( FH.ParseMediaType, // string -> Result[Pair[mediaType, params]] R.Map(PA.Head[string, map[string]string]), R.Fold(F.Constant1[error](false), S.Equals(C.JSON)), ) isJSON(resp.Header.Get(HD.ContentType)) ``` `H.ReadJSON` already performs this validation (JSON or any `+json` type) — no manual check is needed on that path. ## Error Handling Errors from request creation, HTTP status codes, Content-Type validation, and JSON parsing all propagate automatically through the `Result` monad. You only handle errors at the call site: ```go // Pattern 1: run it, then unwrap. pipeline(ctx)() yields a single Result[A]; // result.Unwrap turns that into idiomatic (A, error). value, err := R.Unwrap(pipeline(ctx)()) if err != nil { /* handle */ } // Pattern 2: run the pipeline, then eliminate the Result with result.Fold, // passing named handlers (defined once, see the full example below) F.Pipe1( pipeline(ctx)(), // Result[MyType] R.Fold(writeError(w, http.StatusInternalServerError), writeJSON[MyType](w)), ) ``` **Do not use `RIO.Fold` for this.** In `context/readerioresult`, `Fold[A, B](onLeft Kleisli[error, B], onRight Kleisli[A, B]) Operator[A, B]` stays *inside* the monad — both branches must return a `ReaderIOResult[B]`, so it cannot take plain `func(error)` / `func(A)` side-effecting handlers. To leave the monad, execute the pipeline and fold the resulting `Result` (as above), or use `readerioresult.Fold` from the non-context package, which lands in `ReaderIO`. ## Full HTTP Handler Example ```go package main import ( "encoding/json" "net/http" F "github.com/IBM/fp-go/v2/function" H "github.com/IBM/fp-go/v2/context/readerioresult/http" RIO "github.com/IBM/fp-go/v2/context/readerioresult" R "github.com/IBM/fp-go/v2/result" IO "github.com/IBM/fp-go/v2/io" S "github.com/IBM/fp-go/v2/string" HD "github.com/IBM/fp-go/v2/http/headers" C "github.com/IBM/fp-go/v2/http/content" ) type Post struct { ID int `json:"id"` Title string `json:"title"` } var client = H.MakeClient(http.DefaultClient) // fetchPost: int -> ReaderIOResult[Post], point-free func fetchPost() RIO.Kleisli[int, Post] { return F.Flow4( S.Format[int]("https://jsonplaceholder.typicode.com/posts/%d"), H.MakeGetRequest, H.ReadJSON[Post](client), RIO.TapIOK(IO.Logf[Post]("fetched: %v")), ) } // Leaf handlers — the only place that touches the ResponseWriter func writeError(w http.ResponseWriter, status int) func(error) F.Void { return func(err error) F.Void { http.Error(w, err.Error(), status) return F.VOID } } func writeJSON[A any](w http.ResponseWriter) func(A) F.Void { return func(a A) F.Void { w.Header().Set(HD.ContentType, C.JSON) json.NewEncoder(w).Encode(a) return F.VOID } } func handler(w http.ResponseWriter, r *http.Request) { F.Pipe1( fetchPost()(1)(r.Context())(), // run it: Result[Post] R.Fold(writeError(w, http.StatusBadGateway), writeJSON[Post](w)), ) } ``` ## Import Reference ```go import ( "net/http" H "github.com/IBM/fp-go/v2/context/readerioresult/http" RB "github.com/IBM/fp-go/v2/context/readerioresult/http/builder" B "github.com/IBM/fp-go/v2/http/builder" HD "github.com/IBM/fp-go/v2/http/headers" // header name constants, AtValue/AtValues lenses, Monoid C "github.com/IBM/fp-go/v2/http/content" // content type constants FH "github.com/IBM/fp-go/v2/http" // ParseMediaType, StatusCodeError, … RIO "github.com/IBM/fp-go/v2/context/readerioresult" F "github.com/IBM/fp-go/v2/function" R "github.com/IBM/fp-go/v2/result" // Unwrap, Fold — leaving the monad A "github.com/IBM/fp-go/v2/array" T "github.com/IBM/fp-go/v2/tuple" IO "github.com/IBM/fp-go/v2/io" N "github.com/IBM/fp-go/v2/number" S "github.com/IBM/fp-go/v2/string" PA "github.com/IBM/fp-go/v2/pair" ) ```