Skip to content

Commit

Permalink
feat (sync-service): support OPTIONS requests (#1649)
Browse files Browse the repository at this point in the history
This PR fixes #1646 by
adding support for HTTP OPTIONS requests.
  • Loading branch information
kevin-dp authored Sep 9, 2024
1 parent 0541cac commit a95a269
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/cold-wolves-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@core/sync-service": patch
---

Support OPTIONS request required for preflight requests.
53 changes: 53 additions & 0 deletions packages/sync-service/lib/electric/plug/options_shape_plug.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
defmodule Electric.Plug.OptionsShapePlug do
require Logger
use Plug.Builder

plug :add_allowed_methods_header
plug :add_cache_max_age_header
plug :filter_cors_headers
plug :add_allow_origin_header
plug :add_keep_alive
plug :send_options_response

@allowed_headers ["if-none-match"]

defp add_allowed_methods_header(conn, _),
do: Plug.Conn.put_resp_header(conn, "access-control-allow-methods", "GET, OPTIONS, DELETE")

defp add_cache_max_age_header(conn, _) do
conn
|> Plug.Conn.put_resp_header("access-control-max-age", "86400")
|> Plug.Conn.delete_resp_header("cache-control")
end

# Filters out the unsupported headers from the provided "access-control-request-headers"
defp filter_cors_headers(conn, _) do
case Plug.Conn.get_req_header(conn, "access-control-request-headers") do
[] ->
conn

headers ->
supported_headers =
headers
|> Enum.filter(&Enum.member?(@allowed_headers, String.downcase(&1)))
|> Enum.join(",")

case supported_headers do
"" -> conn
_ -> Plug.Conn.put_resp_header(conn, "access-control-allow-headers", supported_headers)
end
end
end

# Electric currently allows any origin to access the API
defp add_allow_origin_header(conn, _) do
case Plug.Conn.get_req_header(conn, "origin") do
[origin] -> Plug.Conn.put_resp_header(conn, "access-control-allow-origin", origin)
_ -> conn
end
end

defp add_keep_alive(conn, _), do: Plug.Conn.put_resp_header(conn, "connection", "keep-alive")

defp send_options_response(conn, _), do: send_resp(conn, 204, "")
end
1 change: 1 addition & 0 deletions packages/sync-service/lib/electric/plug/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ defmodule Electric.Plug.Router do

get "/v1/shape/:root_table", to: Electric.Plug.ServeShapePlug
delete "/v1/shape/:root_table", to: Electric.Plug.DeleteShapePlug
match "/v1/shape/:root_table", via: :options, to: Electric.Plug.OptionsShapePlug

match _ do
send_resp(conn, 404, "Not found")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
defmodule Electric.Plug.OptionsShapePlugTest do
use ExUnit.Case, async: true

alias Electric.Plug.OptionsShapePlug

@registry Registry.OptionsShapePlugTest
@expected_allowed_methods MapSet.new(["GET", "OPTIONS", "DELETE"])

setup do
start_link_supervised!({Registry, keys: :duplicate, name: @registry})
:ok
end

describe "OptionsShapePlug" do
test "returns allowed methods" do
conn =
Plug.Test.conn("OPTIONS", "/?root_table=foo")
|> OptionsShapePlug.call([])

assert conn.status == 204

allowed_methods =
conn
|> Plug.Conn.get_resp_header("access-control-allow-methods")
|> List.first("")
|> String.split(",")
|> Enum.map(&String.trim/1)
|> MapSet.new()

assert allowed_methods == @expected_allowed_methods
assert Plug.Conn.get_resp_header(conn, "access-control-max-age") == ["86400"]
assert Plug.Conn.get_resp_header(conn, "connection") == ["keep-alive"]
end

test "handles access-control-request-headers" do
header = "If-None-Match"

conn =
Plug.Test.conn("OPTIONS", "/?root_table=foo")
|> Plug.Conn.put_req_header("access-control-request-headers", header)
|> OptionsShapePlug.call([])

assert conn.status == 204
assert Plug.Conn.get_resp_header(conn, "access-control-allow-headers") == [header]
end

test "handles origin header" do
origin = "https://example.com"

conn =
Plug.Test.conn("OPTIONS", "/?root_table=foo")
# also checks that it is case insensitive
|> Plug.Conn.put_req_header("origin", origin)
|> OptionsShapePlug.call([])

assert conn.status == 204
assert Plug.Conn.get_resp_header(conn, "access-control-allow-origin") == [origin]
end
end
end

0 comments on commit a95a269

Please sign in to comment.