Skip to content

Commit b3a725f

Browse files
feat: Add header support (#53)
Adds header support for events. This change includes a bump from `go-test-helpers/v2` to `go-test-helpers/v3` which also requires a min Go version bump to `1.23`.
1 parent 94982df commit b3a725f

18 files changed

+179
-81
lines changed

.github/variables/go-versions.env

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
latest=1.22
2-
penultimate=1.21
3-
min=1.17
1+
latest=1.24
2+
penultimate=1.23
3+
min=1.23

.github/workflows/go-versions.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,8 @@ jobs:
4949
- name: Set Go Version Matrices
5050
id: set-matrix
5151
run: |
52-
echo "all=[\"${{ steps.set-env.outputs.latest }}\",\"${{ steps.set-env.outputs.penultimate }}\",\"${{ steps.set-env.outputs.min }}\"]" >> $GITHUB_OUTPUT
52+
if [ "${{ steps.set-env.outputs.penultimate }}" == "${{ steps.set-env.outputs.min }}" ]; then
53+
echo "all=[\"${{ steps.set-env.outputs.latest }}\",\"${{ steps.set-env.outputs.penultimate }}\"]" >> $GITHUB_OUTPUT
54+
else
55+
echo "all=[\"${{ steps.set-env.outputs.latest }}\",\"${{ steps.set-env.outputs.penultimate }}\",\"${{ steps.set-env.outputs.min }}\"]" >> $GITHUB_OUTPUT
56+
fi

.golangci.yml

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,49 @@
1+
version: "2"
12
run:
2-
deadline: 120s
33
tests: false
4-
54
linters:
65
enable:
76
- bodyclose
87
- dupl
9-
- errcheck
10-
- exportloopref
11-
- goconst
128
- gochecknoglobals
139
- gochecknoinits
1410
- goconst
1511
- gocritic
1612
- gocyclo
1713
- godox
18-
- gofmt
19-
- goimports
2014
- gosec
21-
- gosimple
22-
- govet
23-
- ineffassign
2415
- lll
2516
- misspell
2617
- nakedret
2718
- nolintlint
2819
- prealloc
2920
- revive
3021
- staticcheck
31-
- stylecheck
32-
- typecheck
3322
- unconvert
3423
- unparam
35-
- unused
3624
- whitespace
37-
fast: false
38-
39-
linters-settings:
40-
gofmt:
41-
simplify: false
42-
goimports:
43-
local-prefixes: github.com/launchdarkly
44-
25+
exclusions:
26+
generated: lax
27+
paths:
28+
- third_party$
29+
- builtin$
30+
- examples$
4531
issues:
46-
exclude-use-default: false
4732
max-same-issues: 1000
33+
formatters:
34+
enable:
35+
- gofmt
36+
- goimports
37+
settings:
38+
gofmt:
39+
simplify: false
40+
goimports:
41+
local-prefixes:
42+
- gopkg.in/launchdarkly
43+
- github.com/launchdarkly
44+
exclusions:
45+
generated: lax
46+
paths:
47+
- third_party$
48+
- builtin$
49+
- examples$

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
GOLANGCI_LINT_VERSION=v1.60.1
1+
GOLANGCI_LINT_VERSION=v1.64.5
22

33
LINTER=./bin/golangci-lint
44
LINTER_VERSION_FILE=./bin/.golangci-lint-version-$(GOLANGCI_LINT_VERSION)

contract-tests/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/launchdarkly/eventsource/contract-tests
22

3-
go 1.17
3+
go 1.23
44

55
replace github.com/launchdarkly/eventsource => ../
66

contract-tests/go.sum

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
1-
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
21
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
32
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4-
github.com/launchdarkly/go-test-helpers/v2 v2.2.0 h1:L3kGILP/6ewikhzhdNkHy1b5y4zs50LueWenVF0sBbs=
5-
github.com/launchdarkly/go-test-helpers/v2 v2.2.0/go.mod h1:L7+th5govYp5oKU9iN7To5PgznBuIjBPn+ejqKR0avw=
3+
github.com/launchdarkly/go-test-helpers/v3 v3.1.0 h1:E3bxJMzMoA+cJSF3xxtk2/chr1zshl1ZWa0/oR+8bvg=
4+
github.com/launchdarkly/go-test-helpers/v3 v3.1.0/go.mod h1:Ake5+hZFS/DmIGKx/cizhn5W9pGA7pplcR7xCxWiLIo=
65
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
76
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
8-
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
9-
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
107
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
118
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
12-
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
13-
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
14-
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
15-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
169
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
1710
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

decoder.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ package eventsource
33
import (
44
"bufio"
55
"io"
6+
"net/http"
67
"strconv"
78
"strings"
89
"time"
910
)
1011

1112
type publication struct {
1213
id, event, data, lastEventID string
14+
headers http.Header
1315
retry int64
1416
}
1517

@@ -22,12 +24,21 @@ func (s *publication) Retry() int64 { return s.retry }
2224
// LastEventID is from a separate interface, EventWithLastID
2325
func (s *publication) LastEventID() string { return s.lastEventID }
2426

27+
// Headers is from a separate interface, EventWithHeaders
28+
func (s *publication) Headers() http.Header {
29+
if s.headers == nil {
30+
return nil
31+
}
32+
return s.headers.Clone()
33+
}
34+
2535
// A Decoder is capable of reading Events from a stream.
2636
type Decoder struct {
2737
linesCh <-chan string
2838
errorCh <-chan error
2939
readTimeout time.Duration
3040
lastEventID string
41+
headers http.Header
3142
}
3243

3344
// DecoderOption is a common interface for optional configuration parameters that can be
@@ -48,6 +59,12 @@ func (o lastEventIDDecoderOption) apply(d *Decoder) {
4859
d.lastEventID = string(o)
4960
}
5061

62+
type headersDecoderOption http.Header
63+
64+
func (o headersDecoderOption) apply(d *Decoder) {
65+
d.headers = http.Header(o)
66+
}
67+
5168
// DecoderOptionReadTimeout returns an option that sets the read timeout interval for a
5269
// Decoder when the Decoder is created. If the Decoder does not receive new data within this
5370
// length of time, it will return an error. By default, there is no read timeout.
@@ -62,6 +79,13 @@ func DecoderOptionLastEventID(lastEventID string) DecoderOption {
6279
return lastEventIDDecoderOption(lastEventID)
6380
}
6481

82+
// DecoderOptionHeaders returns an option that sets the Headers property for a
83+
// Decoder when the Decoder is created. This allows access to the HTTP response
84+
// headers for the event.
85+
func DecoderOptionHeaders(headers http.Header) DecoderOption {
86+
return headersDecoderOption(headers)
87+
}
88+
6589
// NewDecoder returns a new Decoder instance that reads events with the given io.Reader.
6690
func NewDecoder(r io.Reader) *Decoder {
6791
bufReader := bufio.NewReader(newNormaliser(r))
@@ -152,6 +176,7 @@ ReadLoop:
152176
}
153177
pub.data = strings.TrimSuffix(pub.data, "\n")
154178
pub.lastEventID = dec.lastEventID
179+
pub.headers = dec.headers
155180
return pub, nil
156181
}
157182

decoder_test.go

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package eventsource
22

33
import (
44
"io"
5+
"net/http"
56
"strings"
67
"testing"
78

@@ -57,6 +58,13 @@ func requireLastEventID(t *testing.T, event Event) string {
5758
return eventWithID.LastEventID()
5859
}
5960

61+
func requireHeaders(t *testing.T, event Event) http.Header {
62+
// necessary because we can't yet add Headers to the basic Event interface; see EventWithHeaders
63+
eventWithHeaders, ok := event.(EventWithHeaders)
64+
require.True(t, ok, "event should have implemented EventWithHeaders")
65+
return eventWithHeaders.Headers()
66+
}
67+
6068
func TestDecoderTracksLastEventID(t *testing.T) {
6169
t.Run("uses last ID that is passed in options", func(t *testing.T) {
6270
inputData := "data: abc\n\n"
@@ -96,8 +104,8 @@ func TestDecoderTracksLastEventID(t *testing.T) {
96104
assert.Equal(t, "def", requireLastEventID(t, event3))
97105
})
98106

99-
t.Run("last ID persists if not overridden", func(t *testing.T) {
100-
inputData := "id: abc\ndata: first\n\ndata: second\n\nid: def\ndata:third\n\n"
107+
t.Run("last ID can be overridden with empty string", func(t *testing.T) {
108+
inputData := "id: abc\ndata: first\n\nid: \ndata: second\n\n"
101109
decoder := NewDecoderWithOptions(strings.NewReader(inputData), DecoderOptionLastEventID("my-id"))
102110

103111
event1, err := decoder.Decode()
@@ -112,32 +120,57 @@ func TestDecoderTracksLastEventID(t *testing.T) {
112120

113121
assert.Equal(t, "second", event2.Data())
114122
assert.Equal(t, "", event2.Id())
115-
assert.Equal(t, "abc", requireLastEventID(t, event2))
123+
assert.Equal(t, "", requireLastEventID(t, event2))
124+
})
125+
}
116126

117-
event3, err := decoder.Decode()
127+
func TestDecoderTracksHeaders(t *testing.T) {
128+
t.Run("event headers is nil if not provided in options", func(t *testing.T) {
129+
inputData := "data: abc\n\n"
130+
131+
decoder := NewDecoderWithOptions(strings.NewReader(inputData))
132+
133+
event, err := decoder.Decode()
118134
require.NoError(t, err)
119135

120-
assert.Equal(t, "third", event3.Data())
121-
assert.Equal(t, "def", event3.Id())
122-
assert.Equal(t, "def", requireLastEventID(t, event3))
136+
assert.Equal(t, "abc", event.Data())
137+
assert.Equal(t, "", event.Id())
138+
assert.Nil(t, requireHeaders(t, event))
123139
})
124140

125-
t.Run("last ID can be overridden with empty string", func(t *testing.T) {
126-
inputData := "id: abc\ndata: first\n\nid: \ndata: second\n\n"
127-
decoder := NewDecoderWithOptions(strings.NewReader(inputData), DecoderOptionLastEventID("my-id"))
141+
t.Run("uses headers that are passed in options", func(t *testing.T) {
142+
inputData := "data: abc\n\n"
143+
headers := http.Header{
144+
"X-Ld-Envid": {"env-id"},
145+
}
128146

129-
event1, err := decoder.Decode()
147+
decoder := NewDecoderWithOptions(strings.NewReader(inputData), DecoderOptionHeaders(headers))
148+
149+
event, err := decoder.Decode()
130150
require.NoError(t, err)
131151

132-
assert.Equal(t, "first", event1.Data())
133-
assert.Equal(t, "abc", event1.Id())
134-
assert.Equal(t, "abc", requireLastEventID(t, event1))
152+
assert.Equal(t, "abc", event.Data())
153+
assert.Equal(t, "", event.Id())
154+
assert.Equal(t, headers, requireHeaders(t, event))
155+
})
135156

136-
event2, err := decoder.Decode()
157+
t.Run("event headers are immutable", func(t *testing.T) {
158+
inputData := "data: abc\n\n"
159+
headers := http.Header{
160+
"X-Ld-Envid": {"env-id"},
161+
}
162+
163+
decoder := NewDecoderWithOptions(strings.NewReader(inputData), DecoderOptionHeaders(headers))
164+
165+
event, err := decoder.Decode()
137166
require.NoError(t, err)
138167

139-
assert.Equal(t, "second", event2.Data())
140-
assert.Equal(t, "", event2.Id())
141-
assert.Equal(t, "", requireLastEventID(t, event2))
168+
eventHeaders := requireHeaders(t, event)
169+
assert.Equal(t, "abc", event.Data())
170+
assert.Equal(t, "", event.Id())
171+
assert.Equal(t, headers, eventHeaders)
172+
173+
eventHeaders.Add("New-Header", "new-value")
174+
assert.NotContains(t, headers, "New-Header")
142175
})
143176
}

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
module github.com/launchdarkly/eventsource
22

3-
go 1.17
3+
go 1.23
44

55
require (
6-
github.com/launchdarkly/go-test-helpers/v2 v2.2.0
6+
github.com/launchdarkly/go-test-helpers/v3 v3.1.0
77
github.com/stretchr/testify v1.6.0
88
)
99

go.sum

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
22
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
33
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4-
github.com/launchdarkly/go-test-helpers/v2 v2.2.0 h1:L3kGILP/6ewikhzhdNkHy1b5y4zs50LueWenVF0sBbs=
5-
github.com/launchdarkly/go-test-helpers/v2 v2.2.0/go.mod h1:L7+th5govYp5oKU9iN7To5PgznBuIjBPn+ejqKR0avw=
4+
github.com/launchdarkly/go-test-helpers/v3 v3.1.0 h1:E3bxJMzMoA+cJSF3xxtk2/chr1zshl1ZWa0/oR+8bvg=
5+
github.com/launchdarkly/go-test-helpers/v3 v3.1.0/go.mod h1:Ake5+hZFS/DmIGKx/cizhn5W9pGA7pplcR7xCxWiLIo=
66
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
77
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
88
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
9-
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
109
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
1110
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
1211
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1312
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
14-
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
15-
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
1613
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
1714
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
1815
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)