Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
36986fe
Detect preflight requests
aseigo Apr 21, 2025
c39c834
Detect client types: grpc, grpcweb, web
aseigo Apr 21, 2025
e536825
Add a CORS header injection Interceptor
aseigo Apr 21, 2025
39353bc
Reintroduce the boolean http_transcode member of the Stream struct
aseigo Apr 21, 2025
ed43898
web can be detected in extract_subtype
aseigo Apr 21, 2025
2da7bb2
change access_type to access_mode, :web to :http_transcoding
aseigo Apr 21, 2025
c25d76a
default opts to an empty list, whitespace fix
aseigo Apr 22, 2025
0361416
add tests for the CORS header interceptor
aseigo Apr 22, 2025
7ea68ab
Assign to a temporary variable and assign that to Stream.http_transcode
aseigo Apr 22, 2025
e547b8d
provide access to request headers in the Stream object
aseigo Apr 23, 2025
61f4a78
only set cors when the sec-fetch-mode header indicates it
aseigo Apr 23, 2025
a22f2fa
another test: if there is NO sec-fetch-mode header
aseigo Apr 23, 2025
bcd4ddb
Make access-control-allow-headers configurable, and optional
aseigo Apr 23, 2025
81e1def
align variable names with http header names
aseigo Apr 23, 2025
f6493d6
Add a CORS example to the README
aseigo Apr 23, 2025
a32654f
format
aseigo Apr 23, 2025
cd5f108
Merge branch 'master' into feature/client-type-detection
sleipnir Apr 23, 2025
0ea8c8e
Verbage.
aseigo Apr 26, 2025
18ad11f
remove default for allow_origin in CORS interceptor
aseigo Apr 26, 2025
b51feda
raise on failed allow_origin type, and test for that
aseigo Apr 27, 2025
0bb4c56
allow_origin, not allow_header
aseigo Apr 27, 2025
5c832ca
test: add missing compile-time test
polvalente Apr 27, 2025
4ba9460
chore: format readme
polvalente Apr 27, 2025
6700d4e
chore: fix Note bold
polvalente Apr 27, 2025
7c34baa
docs: cleanup readme linking
polvalente Apr 27, 2025
99e6593
docs: proper module linking in exdoc
polvalente Apr 27, 2025
3d3933c
fix: do not use Keyword.validate due to Elixir 1.12 support
polvalente Apr 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ An Elixir implementation of [gRPC](http://www.grpc.io/).
- [Usage](#usage)
- [Simple RPC](#simple-rpc)
- [HTTP Transcoding](#http-transcoding)
- [CORS](#cors)
- [Start Application](#start-application)
- [Features](#features)
- [Benchmark](#benchmark)
Expand All @@ -24,13 +25,13 @@ An Elixir implementation of [gRPC](http://www.grpc.io/).

The package can be installed as:

```elixir
def deps do
[
{:grpc, "~> 0.9"}
]
end
```
```elixir
def deps do
[
{:grpc, "~> 0.9"}
]
end
```

## Usage

Expand Down Expand Up @@ -96,7 +97,7 @@ end

We will use this module [in the gRPC server startup section](#start-application).

**__Note:__** For other types of RPC call like streams see [here](interop/lib/interop/server.ex).
**Note:** For other types of RPC call like streams see [here](interop/lib/interop/server.ex).

### **HTTP Transcoding**

Expand Down Expand Up @@ -152,6 +153,7 @@ mix protobuf.generate \
```

3. Enable http_transcode option in your Server module

```elixir
defmodule Helloworld.Greeter.Server do
use GRPC.Server,
Expand All @@ -167,6 +169,23 @@ end

See full application code in [helloworld_transcoding](examples/helloworld_transcoding) example.

### **CORS**

When accessing gRPC from a browser via HTTP transcoding or gRPC-Web, CORS headers may be required for the browser to allow access to the gRPC endpoint. Adding CORS headers can be done by using `GRPC.Server.Interceptors.CORS` as an interceptor in your `GRPC.Endpoint` module, configuring it as decribed in the module documentation:

Example:

```elixir
# Define your endpoint
defmodule Helloworld.Endpoint do
use GRPC.Endpoint

intercept GRPC.Server.Interceptors.Logger
intercept GRPC.Server.Interceptors.CORS, allow_origin: "mydomain.io"
run Helloworld.Greeter.Server
end
```

### **Start Application**

1. Start gRPC Server in your supervisor tree or Application module:
Expand Down Expand Up @@ -231,7 +250,7 @@ The accepted options for configuration are the ones listed on [Mint.HTTP.connect
- [HTTP Transcoding](https://cloud.google.com/endpoints/docs/grpc/transcoding)
- [TLS Authentication](https://grpc.io/docs/guides/auth/#supported-auth-mechanisms)
- [Error handling](https://grpc.io/docs/guides/error/)
- Interceptors (See [`GRPC.Endpoint`](https://github.com/elixir-grpc/grpc/blob/master/lib/grpc/endpoint.ex))
- [Interceptors](`GRPC.Endpoint`)
- [Connection Backoff](https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md)
- Data compression
- [gRPC Reflection](https://github.com/elixir-grpc/grpc-reflection)
Expand Down
15 changes: 10 additions & 5 deletions lib/grpc/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ defmodule GRPC.Server do
path = "/#{service_name}/#{name}"
grpc_type = GRPC.Service.grpc_type(rpc)

def __call_rpc__(unquote(path), :post, stream) do
def __call_rpc__(unquote(path), http_method, stream)
when http_method == :post or http_method == :options do
GRPC.Server.call(
unquote(service_mod),
%{
Expand All @@ -178,8 +179,7 @@ defmodule GRPC.Server do
| service_name: unquote(service_name),
method_name: unquote(to_string(name)),
grpc_type: unquote(grpc_type),
http_method: unquote(http_method),
http_transcode: unquote(http_transcode)
http_method: unquote(http_method)
},
unquote(Macro.escape(put_elem(rpc, 0, func_name))),
unquote(func_name)
Expand Down Expand Up @@ -252,7 +252,7 @@ defmodule GRPC.Server do
codec: codec,
adapter: adapter,
payload: payload,
http_transcode: true
access_mode: :http_transcoding
} = stream,
func_name
) do
Expand All @@ -271,6 +271,10 @@ defmodule GRPC.Server do
end
end

defp do_handle_request(false, res_stream, %{is_preflight?: true} = stream, func_name) do
call_with_interceptors(res_stream, func_name, stream, [])
end

defp do_handle_request(
false,
res_stream,
Expand Down Expand Up @@ -339,7 +343,8 @@ defmodule GRPC.Server do
) do
GRPC.Telemetry.server_span(server, endpoint, func_name, stream, fn ->
last = fn r, s ->
reply = apply(server, func_name, [r, s])
# no response is rquired for preflight requests
reply = if stream.is_preflight?, do: [], else: apply(server, func_name, [r, s])

if res_stream do
{:ok, stream}
Expand Down
81 changes: 49 additions & 32 deletions lib/grpc/server/adapters/cowboy/handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
@type stream_state :: %{
pid: server_rpc_pid :: pid,
handling_timer: timeout_timer_ref :: reference,
pending_reader: nil | pending_reader
pending_reader: nil | pending_reader,
access_mode: GRPC.Server.Stream.access_mode()
}
@type init_result ::
{:cowboy_loop, :cowboy_req.req(), stream_state} | {:ok, :cowboy_req.req(), init_state}
Expand Down Expand Up @@ -56,10 +57,12 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
|> String.downcase()
|> String.to_existing_atom()

with {:ok, sub_type, content_type} <- find_content_type_subtype(req),
with {:ok, access_mode, sub_type, content_type} <- find_content_type_subtype(req),
{:ok, codec} <- find_codec(sub_type, content_type, server),
{:ok, compressor} <- find_compressor(req, server) do
stream_pid = self()
http_transcode = access_mode == :http_transcoding
request_headers = :cowboy_req.headers(req)

stream = %GRPC.Server.Stream{
server: server,
Expand All @@ -69,16 +72,19 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
local: opts[:local],
codec: codec,
http_method: http_method,
http_request_headers: request_headers,
http_transcode: http_transcode,
compressor: compressor,
http_transcode: transcode?(req)
is_preflight?: preflight?(req),
access_mode: access_mode
}

server_rpc_pid = :proc_lib.spawn_link(__MODULE__, :call_rpc, [server, route, stream])
Process.flag(:trap_exit, true)

req = :cowboy_req.set_resp_headers(HTTP2.server_headers(stream), req)

timeout = :cowboy_req.header("grpc-timeout", req)
timeout = Map.get(request_headers, "grpc-timeout")

timer_ref =
if is_binary(timeout) do
Expand All @@ -89,7 +95,16 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
)
end

{:cowboy_loop, req, %{pid: server_rpc_pid, handling_timer: timer_ref, pending_reader: nil}}
{
:cowboy_loop,
req,
%{
pid: server_rpc_pid,
handling_timer: timer_ref,
pending_reader: nil,
access_mode: access_mode
}
}
else
{:error, error} ->
Logger.error(fn -> inspect(error) end)
Expand Down Expand Up @@ -121,12 +136,9 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
content_type
end

find_subtype(content_type)
end

defp find_subtype(content_type) do
{:ok, subtype} = extract_subtype(content_type)
{:ok, subtype, content_type}
{:ok, access_mode, subtype} = extract_subtype(content_type)
access_mode = resolve_access_mode(req, access_mode, subtype)
{:ok, access_mode, subtype, content_type}
end

defp find_compressor(req, server) do
Expand Down Expand Up @@ -600,38 +612,43 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
end
end

defp extract_subtype("application/json"), do: {:ok, "json"}
defp extract_subtype("application/grpc"), do: {:ok, "proto"}
defp extract_subtype("application/grpc+"), do: {:ok, "proto"}
defp extract_subtype("application/grpc;"), do: {:ok, "proto"}
defp extract_subtype(<<"application/grpc+", rest::binary>>), do: {:ok, rest}
defp extract_subtype(<<"application/grpc;", rest::binary>>), do: {:ok, rest}
defp extract_subtype("application/json"), do: {:ok, :http_transcoding, "json"}
defp extract_subtype("application/grpc"), do: {:ok, :grpc, "proto"}
defp extract_subtype("application/grpc+"), do: {:ok, :grpc, "proto"}
defp extract_subtype("application/grpc;"), do: {:ok, :grpc, "proto"}
defp extract_subtype(<<"application/grpc+", rest::binary>>), do: {:ok, :grpc, rest}
defp extract_subtype(<<"application/grpc;", rest::binary>>), do: {:ok, :grpc, rest}

defp extract_subtype("application/grpc-web"), do: {:ok, "proto"}
defp extract_subtype("application/grpc-web+"), do: {:ok, "proto"}
defp extract_subtype("application/grpc-web;"), do: {:ok, "proto"}
defp extract_subtype("application/grpc-web-text"), do: {:ok, "text"}
defp extract_subtype("application/grpc-web+" <> rest), do: {:ok, rest}
defp extract_subtype("application/grpc-web-text+" <> rest), do: {:ok, rest}
defp extract_subtype("application/grpc-web"), do: {:ok, :grpcweb, "proto"}
defp extract_subtype("application/grpc-web+"), do: {:ok, :grpcweb, "proto"}
defp extract_subtype("application/grpc-web;"), do: {:ok, :grpcweb, "proto"}
defp extract_subtype("application/grpc-web-text"), do: {:ok, :grpcweb, "text"}
defp extract_subtype("application/grpc-web+" <> rest), do: {:ok, :grpcweb, rest}
defp extract_subtype("application/grpc-web-text+" <> rest), do: {:ok, :grpcweb, rest}

defp extract_subtype(type) do
Logger.warning("Got unknown content-type #{type}, please create an issue.")
{:ok, "proto"}
{:ok, :grpc, "proto"}
end

defp transcode?(%{version: "HTTP/1.1"}), do: true
defp resolve_access_mode(%{version: "HTTP/1.1"}, _detected_access_mode, _type_subtype),
do: :http_transcoding

defp transcode?(req) do
case find_content_type_subtype(req) do
{:ok, "json", _} -> true
_ -> false
end
end
defp resolve_access_mode(%{method: "OPTIONS"}, _detected_access_mode, _type_subtype),
do: :grpcweb

defp resolve_access_mode(_req, detected_access_mode, _type_subtype), do: detected_access_mode

defp preflight?(%{method: "OPTIONS"}), do: true
defp preflight?(_), do: false

defp send_error(req, error, state, reason) do
trailers = HTTP2.server_trailers(error.status, error.message)

status = if transcode?(req), do: GRPC.Status.http_code(error.status), else: 200
status =
if state.access_mode == :http_transcoding,
do: GRPC.Status.http_code(error.status),
else: 200

if pid = Map.get(state, :pid) do
exit_handler(pid, reason)
Expand Down
Loading
Loading