From d58bec17065f02c975d98ba6dbd90f515746aeec Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 11 Jun 2025 16:30:18 -0600 Subject: [PATCH 01/11] Build with Go 1.24.4 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index cfb595e..0370c17 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/basecamp/thruster -go 1.24.2 +go 1.24.4 require ( github.com/klauspost/compress v1.17.4 From 10e33f6f5a2476231c00a59be209f7a58e98dc1a Mon Sep 17 00:00:00 2001 From: Kevin McConnell Date: Wed, 18 Jun 2025 12:30:03 +0100 Subject: [PATCH 02/11] Bump version --- CHANGELOG.md | 4 ++++ lib/thruster/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b846823..05c6262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## v0.1.14 / 2025-06-18 + +* Build with Go 1.24.4 (#81) + ## v0.1.13 / 2025-04-21 * Update deps to address CVEs (#74) diff --git a/lib/thruster/version.rb b/lib/thruster/version.rb index 0964a9d..55bb65d 100644 --- a/lib/thruster/version.rb +++ b/lib/thruster/version.rb @@ -1,3 +1,3 @@ module Thruster - VERSION = "0.1.13" + VERSION = "0.1.14" end From 55bfd72347a727aa93576cc8229eb5b0806d297b Mon Sep 17 00:00:00 2001 From: Nate Berkopec Date: Fri, 25 Jul 2025 14:11:59 +0900 Subject: [PATCH 03/11] Add X-Request-Start header middleware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds middleware that sets X-Request-Start header with millisecond timestamp for request timing measurement. Header format follows convention of t=. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- internal/handler.go | 1 + internal/handler_test.go | 17 +++++++++++ internal/request_start_middleware.go | 15 ++++++++++ internal/request_start_middleware_test.go | 35 +++++++++++++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 internal/request_start_middleware.go create mode 100644 internal/request_start_middleware_test.go diff --git a/internal/handler.go b/internal/handler.go index c9fbafa..1d016c4 100644 --- a/internal/handler.go +++ b/internal/handler.go @@ -31,6 +31,7 @@ func NewHandler(options HandlerOptions) http.Handler { handler = http.MaxBytesHandler(handler, int64(options.maxRequestBody)) } + handler = NewRequestStartMiddleware(handler) handler = NewLoggingMiddleware(slog.Default(), handler) return handler diff --git a/internal/handler_test.go b/internal/handler_test.go index 77d2584..389241a 100644 --- a/internal/handler_test.go +++ b/internal/handler_test.go @@ -263,6 +263,23 @@ func TestHandlerXForwardedHeadersDropsExistingHeadersWhenForwardingNotEnabled(t h.ServeHTTP(w, r) } +func TestHandlerAddsXRequestStartHeader(t *testing.T) { + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + header := r.Header.Get("X-Request-Start") + assert.NotEmpty(t, header, "X-Request-Start header should be present") + assert.Regexp(t, `^t=\d+$`, header, "X-Request-Start header should be in format t=msec") + })) + defer upstream.Close() + + h := NewHandler(handlerOptions(upstream.URL)) + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + h.ServeHTTP(w, r) + + assert.Equal(t, http.StatusOK, w.Code) +} + // Helpers func handlerOptions(targetUrl string) HandlerOptions { diff --git a/internal/request_start_middleware.go b/internal/request_start_middleware.go new file mode 100644 index 0000000..5f5e7ec --- /dev/null +++ b/internal/request_start_middleware.go @@ -0,0 +1,15 @@ +package internal + +import ( + "fmt" + "net/http" + "time" +) + +func NewRequestStartMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + timestamp := time.Now().UnixMilli() + r.Header.Set("X-Request-Start", fmt.Sprintf("t=%d", timestamp)) + next.ServeHTTP(w, r) + }) +} \ No newline at end of file diff --git a/internal/request_start_middleware_test.go b/internal/request_start_middleware_test.go new file mode 100644 index 0000000..df67bab --- /dev/null +++ b/internal/request_start_middleware_test.go @@ -0,0 +1,35 @@ +package internal + +import ( + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestRequestStartMiddleware(t *testing.T) { + var capturedHeader string + nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedHeader = r.Header.Get("X-Request-Start") + }) + + middleware := NewRequestStartMiddleware(nextHandler) + + before := time.Now().UnixMilli() + req := httptest.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + middleware.ServeHTTP(w, req) + after := time.Now().UnixMilli() + + assert.NotEmpty(t, capturedHeader) + assert.Regexp(t, `^t=\d+$`, capturedHeader) + + timestampStr := capturedHeader[2:] + timestamp, err := strconv.ParseInt(timestampStr, 10, 64) + assert.NoError(t, err) + assert.GreaterOrEqual(t, timestamp, before) + assert.LessOrEqual(t, timestamp, after) +} \ No newline at end of file From af11c36090e1feea91876ce2cd008844e573b0b1 Mon Sep 17 00:00:00 2001 From: Nate Berkopec Date: Sat, 26 Jul 2025 13:25:48 +0900 Subject: [PATCH 04/11] Prevent overwriting existing X-Request-Start header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- internal/request_start_middleware.go | 6 ++++-- internal/request_start_middleware_test.go | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/internal/request_start_middleware.go b/internal/request_start_middleware.go index 5f5e7ec..40f3a69 100644 --- a/internal/request_start_middleware.go +++ b/internal/request_start_middleware.go @@ -8,8 +8,10 @@ import ( func NewRequestStartMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - timestamp := time.Now().UnixMilli() - r.Header.Set("X-Request-Start", fmt.Sprintf("t=%d", timestamp)) + if r.Header.Get("X-Request-Start") == "" { + timestamp := time.Now().UnixMilli() + r.Header.Set("X-Request-Start", fmt.Sprintf("t=%d", timestamp)) + } next.ServeHTTP(w, r) }) } \ No newline at end of file diff --git a/internal/request_start_middleware_test.go b/internal/request_start_middleware_test.go index df67bab..ca2a0b1 100644 --- a/internal/request_start_middleware_test.go +++ b/internal/request_start_middleware_test.go @@ -32,4 +32,21 @@ func TestRequestStartMiddleware(t *testing.T) { assert.NoError(t, err) assert.GreaterOrEqual(t, timestamp, before) assert.LessOrEqual(t, timestamp, after) +} + +func TestRequestStartMiddlewareDoesNotOverwriteExistingHeader(t *testing.T) { + existingHeader := "t=1234567890" + var capturedHeader string + nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedHeader = r.Header.Get("X-Request-Start") + }) + + middleware := NewRequestStartMiddleware(nextHandler) + + req := httptest.NewRequest("GET", "/", nil) + req.Header.Set("X-Request-Start", existingHeader) + w := httptest.NewRecorder() + middleware.ServeHTTP(w, req) + + assert.Equal(t, existingHeader, capturedHeader) } \ No newline at end of file From 95783cfe0a40a558d33e44973be25262f7eae2a0 Mon Sep 17 00:00:00 2001 From: Aleksandr Borisov Date: Sun, 3 Aug 2025 10:11:26 +0300 Subject: [PATCH 05/11] Add host to cache key --- internal/cache_handler_test.go | 47 ++++++++++++++++++++++++++++++++++ internal/variant.go | 1 + 2 files changed, 48 insertions(+) diff --git a/internal/cache_handler_test.go b/internal/cache_handler_test.go index 59c7f42..99302a6 100644 --- a/internal/cache_handler_test.go +++ b/internal/cache_handler_test.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "strings" "testing" "time" @@ -165,6 +166,52 @@ func TestCacheHandler_vary_header(t *testing.T) { assert.Equal(t, "hit", resp.Header().Get("X-Cache")) } +func TestCacheHandler_different_hosts(t *testing.T) { + cache := newTestCache() + handler := NewCacheHandler(cache, 1024, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + host := r.Header.Get("Host") + w.Header().Set("Cache-Control", "public, max-age=600") + w.Write([]byte(host)) + })) + + doReq := func(url string) *httptest.ResponseRecorder { + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", url, nil) + host := strings.Split(url, "://")[1] + r.Header.Set("Host", host) + handler.ServeHTTP(w, r) + return w + } + + resp := doReq("https://example.com") + assert.Equal(t, "example.com", resp.Body.String()) + assert.Equal(t, "miss", resp.Header().Get("X-Cache")) + + resp = doReq("https://example.com") + assert.Equal(t, "example.com", resp.Body.String()) + assert.Equal(t, "hit", resp.Header().Get("X-Cache")) + + resp = doReq("https://another.com") + assert.Equal(t, "another.com", resp.Body.String()) + assert.Equal(t, "miss", resp.Header().Get("X-Cache")) + + resp = doReq("https://another.com") + assert.Equal(t, "another.com", resp.Body.String()) + assert.Equal(t, "hit", resp.Header().Get("X-Cache")) + + resp = doReq("https://example.com/test") + assert.Equal(t, "example.com/test", resp.Body.String()) + assert.Equal(t, "miss", resp.Header().Get("X-Cache")) + + resp = doReq("https://another.com/test") + assert.Equal(t, "another.com/test", resp.Body.String()) + assert.Equal(t, "miss", resp.Header().Get("X-Cache")) + + resp = doReq("https://another.com/test") + assert.Equal(t, "another.com/test", resp.Body.String()) + assert.Equal(t, "hit", resp.Header().Get("X-Cache")) +} + func TestCacheHandler_range_requests_are_not_cached(t *testing.T) { cache := newTestCache() diff --git a/internal/variant.go b/internal/variant.go index 3685075..f34e9e1 100644 --- a/internal/variant.go +++ b/internal/variant.go @@ -25,6 +25,7 @@ func (v *Variant) CacheKey() CacheKey { hash.Write([]byte(v.r.Method)) hash.Write([]byte(v.r.URL.Path)) hash.Write([]byte(v.r.URL.Query().Encode())) + hash.Write([]byte(v.r.Host)) for _, name := range v.headerNames { hash.Write([]byte(name + "=" + v.r.Header.Get(name))) From 6ebd9281126ee04f3a6766c9c2d9f2132803ff8d Mon Sep 17 00:00:00 2001 From: Kevin McConnell Date: Mon, 4 Aug 2025 11:19:33 +0100 Subject: [PATCH 06/11] Update Ruby version --- .ruby-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ruby-version b/.ruby-version index 15a2799..4f5e697 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.3.0 +3.4.5 From 3707aac2daf3fe788d10f1066c327d9ed3fcf79d Mon Sep 17 00:00:00 2001 From: Kevin McConnell Date: Mon, 4 Aug 2025 11:49:14 +0100 Subject: [PATCH 07/11] Ensure all response writers are flushable When using custom response writers in middleware, we need to ensure they all implement `http.Flusher`. Otherwise, the resulting handler will not be flushable, which means the reverse proxy has no way to flush response content to the client. Not being able to flush content breaks response types like SSE, which depend on sending chunked data as it happens, rather than buffering it all to the end. --- internal/cacheable_response.go | 7 +++++++ internal/handler_test.go | 21 +++++++++++++++++++++ internal/logging_middleware.go | 7 +++++++ internal/sendfile_handler.go | 7 +++++++ 4 files changed, 42 insertions(+) diff --git a/internal/cacheable_response.go b/internal/cacheable_response.go index b6906aa..6f9f6f4 100644 --- a/internal/cacheable_response.go +++ b/internal/cacheable_response.go @@ -75,6 +75,13 @@ func (c *CacheableResponse) WriteHeader(statusCode int) { c.headersWritten = true } +func (c *CacheableResponse) Flush() { + flusher, ok := c.responseWriter.(http.Flusher) + if ok { + flusher.Flush() + } +} + func (c *CacheableResponse) CacheStatus() (bool, time.Time) { if c.stasher.Overflowed() { return false, time.Time{} diff --git a/internal/handler_test.go b/internal/handler_test.go index 389241a..4c6a0b1 100644 --- a/internal/handler_test.go +++ b/internal/handler_test.go @@ -2,6 +2,7 @@ package internal import ( "bytes" + "fmt" "net/http" "net/http/httptest" "net/url" @@ -280,6 +281,26 @@ func TestHandlerAddsXRequestStartHeader(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) } +func TestHandlerAllowsFlushingTheResponseBody(t *testing.T) { + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/event-stream") + fmt.Fprintf(w, "data: event\n\n") + w.(http.Flusher).Flush() + + })) + defer upstream.Close() + + h := NewHandler(handlerOptions(upstream.URL)) + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + h.ServeHTTP(w, r) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "text/event-stream", w.Header().Get("Content-Type")) + assert.True(t, w.Flushed) +} + // Helpers func handlerOptions(targetUrl string) HandlerOptions { diff --git a/internal/logging_middleware.go b/internal/logging_middleware.go index 289ecdd..fa9c581 100644 --- a/internal/logging_middleware.go +++ b/internal/logging_middleware.go @@ -87,3 +87,10 @@ func (r *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { } return con, rw, err } + +func (r *responseWriter) Flush() { + flusher, ok := r.ResponseWriter.(http.Flusher) + if ok { + flusher.Flush() + } +} diff --git a/internal/sendfile_handler.go b/internal/sendfile_handler.go index d6a7cf8..9c20547 100644 --- a/internal/sendfile_handler.go +++ b/internal/sendfile_handler.go @@ -79,6 +79,13 @@ func (w *sendfileWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { return hijacker.Hijack() } +func (w *sendfileWriter) Flush() { + flusher, ok := w.w.(http.Flusher) + if ok { + flusher.Flush() + } +} + func (w *sendfileWriter) sendingFilename() string { return w.w.Header().Get("X-Sendfile") } From 796f0e74a3f6f2e837890ea618f4f99a761a018c Mon Sep 17 00:00:00 2001 From: Nogweii Date: Thu, 14 Nov 2024 12:33:46 -0700 Subject: [PATCH 08/11] Teach thruster to optionally not log requests Adds an environment variable, $LOG_REQUESTS, that when set to a falsey value will not add the logging middleware to the HTTP handler, disabling request logs. Other logs, like the startup messages, are left alone. Closes #49. --- internal/config.go | 9 ++++++--- internal/config_test.go | 4 ++++ internal/handler.go | 8 ++++++-- internal/handler_test.go | 1 + internal/service.go | 1 + 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/internal/config.go b/internal/config.go index 655f9eb..117555e 100644 --- a/internal/config.go +++ b/internal/config.go @@ -33,7 +33,8 @@ const ( defaultHttpReadTimeout = 30 * time.Second defaultHttpWriteTimeout = 30 * time.Second - defaultLogLevel = slog.LevelInfo + defaultLogLevel = slog.LevelInfo + defaultLogRequests = true ) type Config struct { @@ -62,7 +63,8 @@ type Config struct { ForwardHeaders bool - LogLevel slog.Level + LogLevel slog.Level + LogRequests bool } func NewConfig() (*Config, error) { @@ -99,7 +101,8 @@ func NewConfig() (*Config, error) { HttpReadTimeout: getEnvDuration("HTTP_READ_TIMEOUT", defaultHttpReadTimeout), HttpWriteTimeout: getEnvDuration("HTTP_WRITE_TIMEOUT", defaultHttpWriteTimeout), - LogLevel: logLevel, + LogLevel: logLevel, + LogRequests: getEnvBool("LOG_REQUESTS", true), } config.ForwardHeaders = getEnvBool("FORWARD_HEADERS", !config.HasTLS()) diff --git a/internal/config_test.go b/internal/config_test.go index 7446693..b8ad054 100644 --- a/internal/config_test.go +++ b/internal/config_test.go @@ -116,6 +116,7 @@ func TestConfig_override_defaults_with_env_vars(t *testing.T) { usingEnvVar(t, "GZIP_COMPRESSION_ENABLED", "0") usingEnvVar(t, "DEBUG", "1") usingEnvVar(t, "ACME_DIRECTORY", "https://acme-staging-v02.api.letsencrypt.org/directory") + usingEnvVar(t, "LOG_REQUESTS", "false") c, err := NewConfig() require.NoError(t, err) @@ -127,6 +128,7 @@ func TestConfig_override_defaults_with_env_vars(t *testing.T) { assert.Equal(t, false, c.GzipCompressionEnabled) assert.Equal(t, slog.LevelDebug, c.LogLevel) assert.Equal(t, "https://acme-staging-v02.api.letsencrypt.org/directory", c.ACMEDirectoryURL) + assert.Equal(t, false, c.LogRequests) } func TestConfig_override_defaults_with_env_vars_using_prefix(t *testing.T) { @@ -136,6 +138,7 @@ func TestConfig_override_defaults_with_env_vars_using_prefix(t *testing.T) { usingEnvVar(t, "THRUSTER_HTTP_READ_TIMEOUT", "5") usingEnvVar(t, "THRUSTER_X_SENDFILE_ENABLED", "0") usingEnvVar(t, "THRUSTER_DEBUG", "1") + usingEnvVar(t, "THRUSTER_LOG_REQUESTS", "0") c, err := NewConfig() require.NoError(t, err) @@ -145,6 +148,7 @@ func TestConfig_override_defaults_with_env_vars_using_prefix(t *testing.T) { assert.Equal(t, 5*time.Second, c.HttpReadTimeout) assert.Equal(t, false, c.XSendfileEnabled) assert.Equal(t, slog.LevelDebug, c.LogLevel) + assert.Equal(t, false, c.LogRequests) } func TestConfig_prefixed_variables_take_precedence_over_non_prefixed(t *testing.T) { diff --git a/internal/handler.go b/internal/handler.go index 1d016c4..16609cf 100644 --- a/internal/handler.go +++ b/internal/handler.go @@ -17,12 +17,15 @@ type HandlerOptions struct { xSendfileEnabled bool gzipCompressionEnabled bool forwardHeaders bool + logRequests bool } func NewHandler(options HandlerOptions) http.Handler { handler := NewProxyHandler(options.targetUrl, options.badGatewayPage, options.forwardHeaders) handler = NewCacheHandler(options.cache, options.maxCacheableResponseBody, handler) handler = NewSendfileHandler(options.xSendfileEnabled, handler) + handler = NewRequestStartMiddleware(handler) + if options.gzipCompressionEnabled { handler = gzhttp.GzipHandler(handler) } @@ -31,8 +34,9 @@ func NewHandler(options HandlerOptions) http.Handler { handler = http.MaxBytesHandler(handler, int64(options.maxRequestBody)) } - handler = NewRequestStartMiddleware(handler) - handler = NewLoggingMiddleware(slog.Default(), handler) + if options.logRequests { + handler = NewLoggingMiddleware(slog.Default(), handler) + } return handler } diff --git a/internal/handler_test.go b/internal/handler_test.go index 389241a..d427e23 100644 --- a/internal/handler_test.go +++ b/internal/handler_test.go @@ -293,5 +293,6 @@ func handlerOptions(targetUrl string) HandlerOptions { maxCacheableResponseBody: 1024, badGatewayPage: "", forwardHeaders: true, + logRequests: true, } } diff --git a/internal/service.go b/internal/service.go index 04fee25..e844783 100644 --- a/internal/service.go +++ b/internal/service.go @@ -27,6 +27,7 @@ func (s *Service) Run() int { maxRequestBody: s.config.MaxRequestBody, badGatewayPage: s.config.BadGatewayPage, forwardHeaders: s.config.ForwardHeaders, + logRequests: s.config.LogRequests, } handler := NewHandler(handlerOptions) From 287f283b42460d39c2307de25152eede4981710f Mon Sep 17 00:00:00 2001 From: Nogweii Date: Thu, 14 Nov 2024 12:47:49 -0700 Subject: [PATCH 09/11] don't hard code the default, use the defined constant --- internal/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/config.go b/internal/config.go index 117555e..f715e8e 100644 --- a/internal/config.go +++ b/internal/config.go @@ -102,7 +102,7 @@ func NewConfig() (*Config, error) { HttpWriteTimeout: getEnvDuration("HTTP_WRITE_TIMEOUT", defaultHttpWriteTimeout), LogLevel: logLevel, - LogRequests: getEnvBool("LOG_REQUESTS", true), + LogRequests: getEnvBool("LOG_REQUESTS", defaultLogRequests), } config.ForwardHeaders = getEnvBool("FORWARD_HEADERS", !config.HasTLS()) From 3ac7e5ca03b5d1a7d89adc27c71a1486fa5a22ec Mon Sep 17 00:00:00 2001 From: Kevin McConnell Date: Tue, 5 Aug 2025 08:29:06 +0100 Subject: [PATCH 10/11] Document `LOG_REQUESTS` in the `README` --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5f4d36e..5da4b77 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ environment variables that you can set. | `EAB_KID` | The EAB key identifier to use when provisioning TLS certificates, if required. | None | | `EAB_HMAC_KEY` | The Base64-encoded EAB HMAC key to use when provisioning TLS certificates, if required. | None | | `FORWARD_HEADERS` | Whether to forward X-Forwarded-* headers from the client. | Disabled when running with TLS; enabled otherwise | +| `LOG_REQUESTS` | Log all requests. Set to `0` or `false` to disable request logging | Enabled | | `DEBUG` | Set to `1` or `true` to enable debug logging. | Disabled | To prevent naming clashes with your application's own environment variables, From 2f696b20de0effb8a29e4c281c38804941c884fc Mon Sep 17 00:00:00 2001 From: Kevin McConnell Date: Tue, 5 Aug 2025 08:43:55 +0100 Subject: [PATCH 11/11] Bump version --- CHANGELOG.md | 7 +++++++ lib/thruster/version.rb | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05c6262..163d88a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## v0.1.15 / 2025-08-05 + +* Ensure responses are flushable (preventing issues with SSE) (#87) +* Add host to cache key (#86) +* Add X-Request-Start header (#85) +* Add `LOG_REQUESTS` option to control request logging (#50) + ## v0.1.14 / 2025-06-18 * Build with Go 1.24.4 (#81) diff --git a/lib/thruster/version.rb b/lib/thruster/version.rb index 55bb65d..4c71429 100644 --- a/lib/thruster/version.rb +++ b/lib/thruster/version.rb @@ -1,3 +1,3 @@ module Thruster - VERSION = "0.1.14" + VERSION = "0.1.15" end