Skip to content
Merged
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ fmt:
$(DRUN) $(GO) fmt ./...

vet:
$(DRUN) $(GO) vet ./.
$(DRUN) $(GO) vet ./...

bash:
$(DRUN) /bin/bash
75 changes: 42 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
# HTTPcache
# httpcache

[![Go Report Card](https://goreportcard.com/badge/github.com/alexmerren/httpcache)](https://goreportcard.com/report/github.com/alexmerren/httpcache)
![Go Version](https://img.shields.io/badge/go%20version-%3E=1.21-61CFDD.svg?style=flat-square)
[![Go Reference](https://pkg.go.dev/badge/github.com/alexmerren/httpcache.svg)](https://pkg.go.dev/github.com/alexmerren/httpcache)

HTTPcache is a fast, local cache for HTTP requests and responses and wraps the default `http.RoundTripper` from Go standard library.
httpcache is a local cache for HTTP requests and responses, wrapping `http.RoundTripper` from Go standard library.

## Features

HTTPcache has a few useful features:
httpcache has a few useful features:

* Store and retrieve HTTP responses for any type of request.
* Expire responses after a customisable time duration.
* Decide when to store responses based on status code.
- Store and retrieve HTTP responses for any type of request;
- Expire responses after a customisable time duration;
- Decide when to store responses based on status code and request method.

If you want to request a feature then open a [GitHub Issue](https://www.github.com/alexmerren/httpcache/issues) today!
If you want to request a feature then please open a [GitHub Issue](https://www.github.com/alexmerren/httpcache/issues) today!

## Quick Start

Expand All @@ -27,33 +27,42 @@ go get -u github.com/alexmerren/httpcache
Here's an example of using the `httpcache` module to cache responses:

```go
package main

func main() {
// Create a new cached round tripper that:
// * Only stores responses with status code 200.
// * Refuses to store responses with status code 404.
cache := httpcache.NewCachedRoundTripper(
httpcache.WithAllowedStatusCodes([]int{200}),
httpcache.WithDeniedStatusCodes([]int{404}),
)

// Create HTTP client with cached round tripper.
httpClient := &http.Client{
Transport: cache,
}

// Do first request to populate local database.
httpClient.Get("https://www.google.com")

// Subsequent requests read from database with no outgoing HTTP request.
for _ = range 10 {
response, _ = httpClient.Get("https://www.google.com")
defer response.Body.Close()
responseBody = io.ReadAll(response.body)

fmt.Println(responseBody)
}
// Create a new SQLite database to store HTTP responses.
cache, _ := httpcache.NewSqliteCache("database.sqlite")

// Create a config with a behaviour of:
// - Storing responses with status code 200;
// - Storing responses from HTTP requests using method "GET";
// - Expiring responses after 7 days...
config := httpcache.NewConfigBuilder().
WithAllowedStatusCodes([]int{http.StatusOK}).
WithAllowedMethods([]string{http.MethodGet}).
WithExpiryTime(time.Duration(60*24*7) * time.Minute).
Build()

// ... or use the default config.
config = httpcache.DefaultConfig

// Create a transport with the SQLite cache and config.
cachedTransport, _ := httpcache.NewTransport(config, cache)

// Create a HTTP client with the cached roundtripper.
httpClient := http.Client{
Transport: cachedTransport,
}

// Do first request to populate local database.
httpClient.Get("https://www.google.com")

// Subsequent requests read from database with no outgoing HTTP request.
for _ = range 10 {
response, _ := httpClient.Get("https://www.google.com")
defer response.Body.Close()
responseBody, _ := io.ReadAll(response.Body)

fmt.Println(string(responseBody))
}
}
```

Expand Down
35 changes: 35 additions & 0 deletions cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package httpcache

import (
"context"
"errors"
"net/http"
"time"
)

// Cache is the entrypoint for saving and reading responses. This can be
// implemented for a custom method to cache responses.
type Cache interface {

// Save a response for a HTTP request using [context.Background].
Save(response *http.Response, expiryTime *time.Duration) error

// Read a saved response for a HTTP request using [context.Background].
Read(request *http.Request) (*http.Response, error)

// Save a response for a HTTP request with a [context.Context]. expiryTime
// is the duration from [time.Now] to expire the response.
SaveContext(ctx context.Context, response *http.Response, expiryTime *time.Duration) error

// Read a saved response for a HTTP request with a [context.Context]. If no
// response is saved for the corresponding request, or the expiryTime has
// been surpassed, then return [ErrNoResponse].
ReadContext(ctx context.Context, request *http.Request) (*http.Response, error)
}

var (
// ErrNoResponse describes when the cache does not have a response stored.
// [Transport] will check if ErrNoResponse is returned from [Cache.Read]. If
// ErrNoResponse is returned, then the request/response will be saved with [Save].
ErrNoResponse = errors.New("no stored response")
)
44 changes: 44 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package httpcache

import (
"net/http"
"time"
)

var (
defaultAllowedStatusCodes = []int{http.StatusOK}
defaultAllowedMethods = []string{http.MethodGet}
defaultExpiryTime = time.Duration(60*24*7) * time.Minute
)

// DefaultConfig creates a [Config] with default values, namely:
// - AllowedStatusCodes: [http.StatusOK]
// - AllowedMethods: [http.MethodGet]
// - ExpiryTime: 7 days.
var DefaultConfig = NewConfigBuilder().
WithAllowedStatusCodes(defaultAllowedStatusCodes).
WithAllowedMethods(defaultAllowedMethods).
WithExpiryTime(defaultExpiryTime).
Build()

// Config describes the configuration to use when saving and reading responses
// from [Cache] using the [Transport].
type Config struct {

// AllowedStatusCodes describes if a HTTP response should be saved by
// checking that it's status code is accepted by [Cache]. If the HTTP
// response's status code is not in AllowedStatusCodes, then do not persist.
//
// This is a required field.
AllowedStatusCodes []int

// AllowedMethods describes if a HTTP response should be saved by checking
// if the HTTP request's method is accepted by the [Cache]. If the HTTP
// request's method is not in AllowedMethods, then do not persist.
//
// This is a required field.
AllowedMethods []string

// ExpiryTime describes when a HTTP response should be considered invalid.
ExpiryTime *time.Duration
}
58 changes: 58 additions & 0 deletions config_builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package httpcache

import "time"

// If adding new fields to [Config], then add the corresponding field and
// methods to [configBuilder]. Configuration values in [configBuilder] always
// use pointer types. This allows the [Config] decide which fields are
// required.

// configBuilder is an internal structure to create configs with required
// parameters.
type configBuilder struct {

// allowedStatusCodes only persists HTTP responses that have an appropriate
// status code (i.e. 200).
//
// This is a required field.
allowedStatusCodes *[]int

// allowedMethods only persists HTTP responses that use an appropriate HTTP
// method (i.e. "GET").
//
// This is a required field.
allowedMethods *[]string

// expiryTime invalidates HTTP responses after a duration has elapsed from
// [time.Now]. Set to nil for no expiry.
expiryTime *time.Duration
}

func NewConfigBuilder() *configBuilder {
return &configBuilder{}
}

func (c *configBuilder) WithAllowedStatusCodes(allowedStatusCodes []int) *configBuilder {
c.allowedStatusCodes = &allowedStatusCodes
return c
}

func (c *configBuilder) WithAllowedMethods(allowedMethods []string) *configBuilder {
c.allowedMethods = &allowedMethods
return c
}

func (c *configBuilder) WithExpiryTime(expiryDuration time.Duration) *configBuilder {
c.expiryTime = &expiryDuration
return c
}

// Build constructs a [Config] that is ready to be consumed by [Transport]. If
// the configuration passed by [configBuilder] is invalid, it will panic.
func (c *configBuilder) Build() *Config {
return &Config{
AllowedStatusCodes: *c.allowedStatusCodes,
AllowedMethods: *c.allowedMethods,
ExpiryTime: c.expiryTime,
}
}
34 changes: 0 additions & 34 deletions file.go

This file was deleted.

45 changes: 0 additions & 45 deletions options.go

This file was deleted.

Loading