Skip to content

Commit 7f43ab0

Browse files
authored
Both support GET and POST HTTP requests for SSE subscription (#52)
This PR brings minor changes to SSE subscription to prevent browsers from automatically trying to reconnect with their previous authenticated headers after a long inactivity period: - Support both GET and POST http methods - this should be temporary to allow clients to migrate to HTTP posts, then `get` support should be removed - Add extra cache directives.
1 parent c121848 commit 7f43ab0

File tree

6 files changed

+78
-30
lines changed

6 files changed

+78
-30
lines changed

neurow/integration_test/message_brokering_test.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ defmodule Neurow.IntegrationTest.MessageBrokeringTest do
3434

3535
assert_headers(headers, [
3636
{"access-control-allow-origin", "*"},
37-
{"cache-control", "no-cache, no-store"},
37+
{"cache-control", "no-cache, no-store, max-age=0, must-revalidate"},
3838
{"connection", "close"},
3939
{"content-type", "text/event-stream"},
4040
{"transfer-encoding", "chunked"}
@@ -91,7 +91,7 @@ defmodule Neurow.IntegrationTest.MessageBrokeringTest do
9191

9292
assert_headers(headers, [
9393
{"access-control-allow-origin", "*"},
94-
{"cache-control", "no-cache, no-store"},
94+
{"cache-control", "no-cache, no-store, max-age=0, must-revalidate"},
9595
{"connection", "close"},
9696
{"content-type", "text/event-stream"},
9797
{"transfer-encoding", "chunked"}

neurow/integration_test/message_history_test.exs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ defmodule Neurow.IntegrationTest.MessageHistoryTest do
4646

4747
assert_headers(headers, [
4848
{"access-control-allow-origin", "*"},
49-
{"cache-control", "no-cache, no-store"},
49+
{"cache-control", "no-cache, no-store, max-age=0, must-revalidate"},
5050
{"connection", "close"},
5151
{"content-type", "text/event-stream"},
5252
{"transfer-encoding", "chunked"}
@@ -70,9 +70,9 @@ defmodule Neurow.IntegrationTest.MessageHistoryTest do
7070

7171
assert_headers(headers, [
7272
{"access-control-allow-origin", "*"},
73-
{"cache-control", "no-cache, no-store"},
73+
{"cache-control", "no-cache, no-store, max-age=0, must-revalidate"},
7474
{"connection", "close"},
75-
{"content-type", "text/event-stream"},
75+
{"content-type", "text/event-stream"}
7676
])
7777

7878
assert_receive %HTTPoison.AsyncChunk{chunk: body}
@@ -112,7 +112,7 @@ defmodule Neurow.IntegrationTest.MessageHistoryTest do
112112

113113
assert_headers(headers, [
114114
{"access-control-allow-origin", "*"},
115-
{"cache-control", "no-cache, no-store"},
115+
{"cache-control", "no-cache, no-store, max-age=0, must-revalidate"},
116116
{"connection", "close"},
117117
{"content-type", "text/event-stream"},
118118
{"transfer-encoding", "chunked"}
@@ -139,7 +139,7 @@ defmodule Neurow.IntegrationTest.MessageHistoryTest do
139139

140140
assert_headers(headers, [
141141
{"access-control-allow-origin", "*"},
142-
{"cache-control", "no-cache, no-store"},
142+
{"cache-control", "no-cache, no-store, max-age=0, must-revalidate"},
143143
{"connection", "close"},
144144
{"content-type", "text/event-stream"},
145145
{"transfer-encoding", "chunked"}
@@ -171,7 +171,7 @@ defmodule Neurow.IntegrationTest.MessageHistoryTest do
171171

172172
assert_headers(headers, [
173173
{"access-control-allow-origin", "*"},
174-
{"cache-control", "no-cache, no-store"},
174+
{"cache-control", "no-cache, no-store, max-age=0, must-revalidate"},
175175
{"connection", "close"},
176176
{"content-type", "text/event-stream"},
177177
{"transfer-encoding", "chunked"}

neurow/integration_test/sse_livecycle_test.exs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ defmodule Neurow.IntegrationTest.SseLifecycleTest do
33

44
import SseHelper
55
import SseHelper.HttpSse
6+
import JwtHelper
67

78
alias Neurow.IntegrationTest.TestCluster
89

@@ -25,7 +26,7 @@ defmodule Neurow.IntegrationTest.SseLifecycleTest do
2526

2627
assert_headers(headers, [
2728
{"access-control-allow-origin", "*"},
28-
{"cache-control", "no-cache, no-store"},
29+
{"cache-control", "no-cache, no-store, max-age=0, must-revalidate"},
2930
{"connection", "close"},
3031
{"content-type", "text/event-stream"},
3132
{"transfer-encoding", "chunked"}
@@ -59,7 +60,7 @@ defmodule Neurow.IntegrationTest.SseLifecycleTest do
5960
headers,
6061
[
6162
{"access-control-allow-origin", "*"},
62-
{"cache-control", "no-cache, no-store"},
63+
{"cache-control", "no-cache, no-store, max-age=0, must-revalidate"},
6364
{"connection", "close"},
6465
{"content-type", "text/event-stream"},
6566
{"transfer-encoding", "chunked"},
@@ -127,6 +128,30 @@ defmodule Neurow.IntegrationTest.SseLifecycleTest do
127128
end
128129
end
129130

131+
test "also supports GET HTTP requests for SSE subscription", %{
132+
cluster_state: %{
133+
public_api_ports: [first_public_port | _other_ports]
134+
}
135+
} do
136+
headers =
137+
[Authorization: "Bearer #{compute_jwt_token_in_req_header_public_api("test_topic")}"]
138+
139+
async_response = HTTPoison.get!(subscribe_url(first_public_port), headers, stream_to: self())
140+
141+
assert_receive %HTTPoison.AsyncStatus{code: 200}
142+
assert_receive %HTTPoison.AsyncHeaders{headers: headers}
143+
144+
assert_headers(headers, [
145+
{"access-control-allow-origin", "*"},
146+
{"cache-control", "no-cache, no-store, max-age=0, must-revalidate"},
147+
{"connection", "close"},
148+
{"content-type", "text/event-stream"},
149+
{"transfer-encoding", "chunked"}
150+
])
151+
152+
:hackney.stop_async(async_response.id)
153+
end
154+
130155
def override_timeout(timeout) do
131156
{:ok, default_timeout} = Application.fetch_env(:neurow, :sse_timeout)
132157
TestCluster.update_sse_timeout(timeout)

neurow/lib/neurow/public_api/endpoint.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ defmodule Neurow.PublicApi.Endpoint do
2525
context_path = Neurow.Configuration.public_api_context_path()
2626

2727
case {conn.method, conn.request_path} do
28-
{"GET", ^context_path <> "/v1/subscribe"} ->
28+
{method, ^context_path <> "/v1/subscribe"} when method in ["GET", "POST"] ->
2929
subscribe(conn)
3030

3131
_ ->
@@ -45,7 +45,7 @@ defmodule Neurow.PublicApi.Endpoint do
4545
conn
4646
|> put_resp_header("content-type", "text/event-stream")
4747
|> put_resp_header("access-control-allow-origin", "*")
48-
|> put_resp_header("cache-control", "no-cache, no-store")
48+
|> put_resp_header("cache-control", "no-cache, no-store, max-age=0, must-revalidate")
4949
|> put_resp_header("connection", "close")
5050
|> put_resp_header("x-sse-timeout", to_string(timeout_ms))
5151
|> put_resp_header("x-sse-keepalive", to_string(keep_alive_ms))

neurow/test/neurow/public_api/endpoint_test.exs

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ defmodule Neurow.PublicApi.EndpointTest do
99
describe "authentication" do
1010
test "denies access if no JWT token is provided" do
1111
conn =
12-
conn(:get, "/v1/subscribe")
12+
conn(:post, "/v1/subscribe")
1313

1414
call(Neurow.PublicApi.Endpoint, conn, fn ->
1515
assert_receive {:send_resp_status, 401}
@@ -33,7 +33,7 @@ defmodule Neurow.PublicApi.EndpointTest do
3333

3434
test "denies access if an invalid JWT token is provided" do
3535
conn =
36-
conn(:get, "/v1/subscribe")
36+
conn(:post, "/v1/subscribe")
3737
|> put_req_header("authorization", "Bearer bad_token")
3838

3939
call(Neurow.PublicApi.Endpoint, conn, fn ->
@@ -57,7 +57,7 @@ defmodule Neurow.PublicApi.EndpointTest do
5757

5858
test "allows access if a valid JWT token is provided" do
5959
conn =
60-
conn(:get, "/v1/subscribe")
60+
conn(:post, "/v1/subscribe")
6161
|> put_req_header(
6262
"authorization",
6363
"Bearer #{compute_jwt_token_in_req_header_public_api("foo56")}"
@@ -72,7 +72,7 @@ defmodule Neurow.PublicApi.EndpointTest do
7272
describe "messaging" do
7373
test "transmits messages for the subscribed topic" do
7474
conn =
75-
conn(:get, "/v1/subscribe")
75+
conn(:post, "/v1/subscribe")
7676
|> put_req_header(
7777
"authorization",
7878
"Bearer #{compute_jwt_token_in_req_header_public_api("test_topic1")}"
@@ -113,7 +113,7 @@ defmodule Neurow.PublicApi.EndpointTest do
113113

114114
test "returns a bad request error if the Last-Event_Id header is not an integer" do
115115
conn =
116-
conn(:get, "/v1/subscribe")
116+
conn(:post, "/v1/subscribe")
117117
|> put_req_header(
118118
"authorization",
119119
"Bearer #{compute_jwt_token_in_req_header_public_api("test_topic1")}"
@@ -152,7 +152,7 @@ defmodule Neurow.PublicApi.EndpointTest do
152152
publish_message("test_issuer1-test_topic1", 8, "Message ID8")
153153

154154
conn =
155-
conn(:get, "/v1/subscribe")
155+
conn(:post, "/v1/subscribe")
156156
|> put_req_header(
157157
"authorization",
158158
"Bearer #{compute_jwt_token_in_req_header_public_api("test_topic1")}"
@@ -169,7 +169,7 @@ defmodule Neurow.PublicApi.EndpointTest do
169169
publish_message("test_issuer1-other_topic", 7, "This message is not expected")
170170

171171
conn =
172-
conn(:get, "/v1/subscribe")
172+
conn(:post, "/v1/subscribe")
173173
|> put_req_header(
174174
"authorization",
175175
"Bearer #{compute_jwt_token_in_req_header_public_api("test_topic1")}"
@@ -194,7 +194,7 @@ defmodule Neurow.PublicApi.EndpointTest do
194194
publish_message("test_issuer1-test_topic1", 8, "Message ID8")
195195

196196
conn =
197-
conn(:get, "/v1/subscribe")
197+
conn(:post, "/v1/subscribe")
198198
|> put_req_header(
199199
"authorization",
200200
"Bearer #{compute_jwt_token_in_req_header_public_api("test_topic1")}"
@@ -239,7 +239,7 @@ defmodule Neurow.PublicApi.EndpointTest do
239239
publish_message("test_issuer1-test_topic1", 8, "Message ID8")
240240

241241
conn =
242-
conn(:get, "/v1/subscribe")
242+
conn(:post, "/v1/subscribe")
243243
|> put_req_header(
244244
"authorization",
245245
"Bearer #{compute_jwt_token_in_req_header_public_api("test_topic1")}"
@@ -263,7 +263,7 @@ defmodule Neurow.PublicApi.EndpointTest do
263263
publish_message("test_issuer1-test_topic1", 8, "Message ID8")
264264

265265
conn =
266-
conn(:get, "/v1/subscribe")
266+
conn(:post, "/v1/subscribe")
267267
|> put_req_header(
268268
"authorization",
269269
"Bearer #{compute_jwt_token_in_req_header_public_api("test_topic1")}"
@@ -301,7 +301,7 @@ defmodule Neurow.PublicApi.EndpointTest do
301301
publish_message("test_issuer1-test_topic1", 8, "Message ID8")
302302

303303
conn =
304-
conn(:get, "/v1/subscribe")
304+
conn(:post, "/v1/subscribe")
305305
|> put_req_header(
306306
"authorization",
307307
"Bearer #{compute_jwt_token_in_req_header_public_api("test_topic1")}"
@@ -341,7 +341,7 @@ defmodule Neurow.PublicApi.EndpointTest do
341341
override_timeout(500)
342342

343343
conn =
344-
conn(:get, "/v1/subscribe")
344+
conn(:post, "/v1/subscribe")
345345
|> put_req_header(
346346
"authorization",
347347
"Bearer #{compute_jwt_token_in_req_header_public_api("test_topic1")}"
@@ -357,7 +357,7 @@ defmodule Neurow.PublicApi.EndpointTest do
357357
override_keepalive(500)
358358

359359
conn =
360-
conn(:get, "/v1/subscribe")
360+
conn(:post, "/v1/subscribe")
361361
|> put_req_header(
362362
"authorization",
363363
"Bearer #{compute_jwt_token_in_req_header_public_api("test_topic1")}"
@@ -379,7 +379,7 @@ defmodule Neurow.PublicApi.EndpointTest do
379379

380380
test "the client is disconnected when the JWT token expires" do
381381
conn =
382-
conn(:get, "/v1/subscribe")
382+
conn(:post, "/v1/subscribe")
383383
|> put_req_header(
384384
"authorization",
385385
"Bearer #{compute_jwt_token_in_req_header_public_api("test_topic1", duration_s: 3)}"
@@ -466,7 +466,7 @@ defmodule Neurow.PublicApi.EndpointTest do
466466

467467
test "the authentication logic is applied to urls prefixed by the context path" do
468468
conn =
469-
conn(:get, "/v1/subscribe")
469+
conn(:post, "/v1/subscribe")
470470

471471
call(Neurow.PublicApi.Endpoint, conn, fn ->
472472
assert_receive {:send_resp_status, 401}
@@ -490,7 +490,7 @@ defmodule Neurow.PublicApi.EndpointTest do
490490

491491
test "The subscribe url is prefixed with the context path" do
492492
conn =
493-
conn(:get, "/context_path/v1/subscribe")
493+
conn(:post, "/context_path/v1/subscribe")
494494
|> put_req_header(
495495
"authorization",
496496
"Bearer #{compute_jwt_token_in_req_header_public_api("test_topic1")}"
@@ -511,6 +511,29 @@ defmodule Neurow.PublicApi.EndpointTest do
511511
end
512512
end
513513

514+
test "also support GET HTTP requests for SSE subscription" do
515+
conn =
516+
conn(:get, "/v1/subscribe")
517+
|> put_req_header(
518+
"authorization",
519+
"Bearer #{compute_jwt_token_in_req_header_public_api("test_topic1")}"
520+
)
521+
522+
call(Neurow.PublicApi.Endpoint, conn, fn ->
523+
assert_receive {:send_chunked, 200}
524+
525+
publish_message("test_issuer1-test_topic1", 1234, "Message")
526+
527+
assert_receive {:chunk, first_event}
528+
529+
assert parse_sse_event(first_event) == %{
530+
id: "1234",
531+
event: "test-event",
532+
data: "Message"
533+
}
534+
end)
535+
end
536+
514537
defp override_timeout(timeout) do
515538
default_timeout = Application.fetch_env!(:neurow, :sse_timeout)
516539
Application.put_env(:neurow, :sse_timeout, timeout)

neurow/test/sse_helper.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
defmodule SseHelper do
22
@moduledoc """
3-
Provides Helper function to test SSE connections
3+
Provides Helper functions to test SSE connections
44
- Functions at the root of the modules can be used both in unit and integration tests
55
- Functions in SseHelper.PlugSse help to test Plug endpoint in unit test,
66
- Functions in SSeHelper.HttpSse help to test Neurow though actual HTTP connections in integration tests
@@ -121,7 +121,7 @@ defmodule SseHelper do
121121
[Authorization: "Bearer #{compute_jwt_token_in_req_header_public_api(topic)}"] ++
122122
extra_headers
123123

124-
async_response = HTTPoison.get!(subscribe_url(port), headers, stream_to: self())
124+
async_response = HTTPoison.post!(subscribe_url(port), "", headers, stream_to: self())
125125
assert_fn.()
126126
:hackney.stop_async(async_response.id)
127127
end

0 commit comments

Comments
 (0)