package with import ( "bytes" "context" "database/sql" "encoding/json" "fmt" "io" "net/http" "net/url" "time" ) type RoundTripper struct { http.RoundTripper kv SQLKV } func CachedHTTP(ctx context.Context, foo func(*http.Client) error) error { return Sqlite(ctx, ":memory:", func(db *sql.DB) error { return KV(ctx, db, func(kv SQLKV) error { return foo(&http.Client{ Timeout: time.Minute, Transport: RoundTripper{ RoundTripper: &http.Transport{ DisableKeepAlives: true, }, kv: kv, }, }) }) }) } func (c RoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { req := newCacheableRequest(r) if v, err := c.kv.Get(r.Context(), req.cacheK()); err != nil { } else if resp := parseCacheableResponse(v); resp != nil { return resp.response(), nil } resp, err := c.RoundTripper.RoundTrip(r) if err != nil { return nil, err } c.kv.Set(r.Context(), req.cacheK(), newCacheableResponse(resp).cacheV()) return resp, nil } type cacheableRequest struct { URLHost string URLPath string URLQuery cacheableHTTPHeader Header cacheableHTTPHeader Body string } func newCacheableRequest(r *http.Request) cacheableRequest { defer r.Body.Close() b, _ := io.ReadAll(r.Body) r.Body = io.NopCloser(bytes.NewReader(b)) return cacheableRequest{ URLHost: r.URL.Host, URLPath: r.URL.Path, URLQuery: newCacheableHTTPHeader(r.URL.Query()), Header: newCacheableHTTPHeader(r.Header), Body: string(b), } } func (c cacheableRequest) cacheK() string { return fmt.Sprint(c) } type cacheableResponse struct { Code int Header cacheableHTTPHeader Body string } func newCacheableResponse(resp *http.Response) cacheableResponse { defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) resp.Body = io.NopCloser(bytes.NewReader(b)) return cacheableResponse{ Code: resp.StatusCode, Header: newCacheableHTTPHeader(resp.Header), Body: string(b), } } func parseCacheableResponse(b []byte) *cacheableResponse { var c cacheableResponse if err := json.Unmarshal(b, &c); err != nil { return nil } return &c } func (c cacheableResponse) cacheV() []byte { b, _ := json.Marshal(c) return b } func (c cacheableResponse) response() *http.Response { return &http.Response{ StatusCode: c.Code, Header: c.Header.header(), Body: io.NopCloser(bytes.NewReader([]byte(c.Body))), } } type cacheableHTTPHeader [][]string func newCacheableHTTPHeader(m map[string][]string) cacheableHTTPHeader { result := [][]string{} for k, v := range m { result = append(result, append([]string{k}, v...)) } return result } func (c cacheableHTTPHeader) header() http.Header { return http.Header(c.m()) } func (c cacheableHTTPHeader) query() url.Values { return url.Values(c.m()) } func (c cacheableHTTPHeader) m() map[string][]string { m := map[string][]string{} for _, v := range c { v := v m[v[0]] = v[1:] } return m }