Skip to content
Closed
Changes from all commits
Commits
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
120 changes: 86 additions & 34 deletions lib/plug/parsers/json.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,23 @@ defmodule Plug.Parsers.JSON do

@behaviour Plug.Parsers

@decoder_key {__MODULE__, :decoder}
@body_reader_key {__MODULE__, :body_reader}

@impl true
def init(opts) do
{decoder, opts} = Keyword.pop(opts, :json_decoder)
{body_reader, opts} = Keyword.pop(opts, :body_reader, {Plug.Conn, :read_body, []})
decoder = validate_decoder!(decoder)
{body_reader, decoder, opts}

decoder_fun = build_decoder_fun(validate_decoder!(decoder))
body_reader_fun = build_body_reader_fun(validate_body_reader!(body_reader))

:persistent_term.put(@decoder_key, decoder_fun)
:persistent_term.put(@body_reader_key, body_reader_fun)

nest_all = Keyword.get(opts, :nest_all_json, false)

{nest_all, opts}
end

defp validate_decoder!(nil) do
Expand All @@ -59,58 +70,99 @@ defmodule Plug.Parsers.JSON do
mfa
end

defp validate_decoder!(decoder) when is_atom(decoder) do
validate_decoder!({decoder, :decode!, []})
defp validate_decoder!(module) when is_atom(module) do
ensure_compiled_and_exported(module, :decode!, 1)
end

defp validate_decoder!(decoder) do
raise ArgumentError,
defp validate_decoder!(other) do
raise ArgumentError,
"the :json_decoder option expects a module, or a three-element " <>
"tuple in the form of {module, function, extra_args}, got: #{inspect(decoder)}"
"tuple in the form of {module, function, extra_args}, got: #{inspect(other)}"
end

@impl true
def parse(conn, "application", subtype, _headers, {{mod, fun, args}, decoder, opts}) do
if subtype == "json" or String.ends_with?(subtype, "+json") do
apply(mod, fun, [conn, opts | args]) |> decode(decoder, opts)
defp validate_body_reader!({module, fun, args} = mfa)
when is_atom(module) and is_atom(fun) and is_list(args) do
ensure_compiled_and_exported(module, fun, length(args) + 2)
mfa
end

defp ensure_compiled_and_exported(module, fun, arity) do
if Code.ensure_compiled(module) != {:module, module} do
raise ArgumentError,
"invalid :json_decoder option. The module #{inspect(module)} is not " <>
"loaded and could not be found"
end

if function_exported?(module, fun, arity) do
module
else
{:next, conn}
raise ArgumentError,
"invalid :json_decoder option. The module #{inspect(module)} must implement #{fun}/#{arity}"
end
end

def parse(conn, _type, _subtype, _headers, _opts) do
{:next, conn}
# Fast path: module.decode!/1
defp build_decoder_fun(module) when is_atom(module) do
fn body -> module.decode!(body) end
end

defp decode({:ok, "", conn}, _decoder, _opts) do
{:ok, %{}, conn}
# MFA path: apply(module, fun, [body | args])
defp build_decoder_fun({module, fun, args}) do
fn body ->
apply(module, fun, [body | args])
end
end

defp decode({:ok, body, conn}, {module, fun, args}, opts) do
nest_all = Keyword.get(opts, :nest_all_json, false)
defp build_body_reader_fun({module, fun, args}) do
fn conn, opts ->
apply(module, fun, [conn, opts | args])
end
end

try do
apply(module, fun, [body | args])
rescue
e -> raise Plug.Parsers.ParseError, exception: e
else
terms when is_map(terms) and not nest_all ->
{:ok, terms, conn}
@impl true
def parse(conn, "application", subtype, _headers, {nest_all, opts})
when subtype == "json" or binary_part(subtype, byte_size(subtype) - 5, 5) == "+json" do

body_reader_fun = :persistent_term.get(@body_reader_key)

case body_reader_fun.(conn, opts) do
{:ok, "", conn} ->
{:ok, %{}, conn}

terms ->
{:ok, %{"_json" => terms}, conn}
{:ok, body, conn} ->
decode_body(body, conn, nest_all)

{:more, _, conn} ->
{:error, :too_large, conn}

{:error, :timeout} ->
raise Plug.TimeoutError

{:error, _} ->
raise Plug.BadRequestError
end
end

defp decode({:more, _, conn}, _decoder, _opts) do
{:error, :too_large, conn}
def parse(conn, _type, _subtype, _headers, _state) do
{:next, conn}
end

defp decode({:error, :timeout}, _decoder, _opts) do
raise Plug.TimeoutError
end
defp decode_body(body, conn, nest_all) do
decoder_fun = :persistent_term.get(@decoder_key)

defp decode({:error, _}, _decoder, _opts) do
raise Plug.BadRequestError
try do
terms = decoder_fun.(body)

cond do
is_map(terms) and not nest_all ->
{:ok, terms, conn}

true ->
{:ok, %{"_json" => terms}, conn}
end
rescue
e ->
raise Plug.Parsers.ParseError, exception: e
end
end
end