Merge pull request #13078 from zhongwencool/http-415
feat: return 415 when UNSUPPORTED_MEDIA_TYPE
This commit is contained in:
commit
e5da4aa128
|
@ -86,5 +86,6 @@
|
||||||
{'SOURCE_ERROR', <<"Source error">>},
|
{'SOURCE_ERROR', <<"Source error">>},
|
||||||
{'UPDATE_FAILED', <<"Update failed">>},
|
{'UPDATE_FAILED', <<"Update failed">>},
|
||||||
{'REST_FAILED', <<"Reset source or config failed">>},
|
{'REST_FAILED', <<"Reset source or config failed">>},
|
||||||
{'CLIENT_NOT_RESPONSE', <<"Client not responding">>}
|
{'CLIENT_NOT_RESPONSE', <<"Client not responding">>},
|
||||||
|
{'UNSUPPORTED_MEDIA_TYPE', <<"Unsupported media type">>}
|
||||||
]).
|
]).
|
||||||
|
|
|
@ -82,7 +82,9 @@
|
||||||
namespace() -> "bridge".
|
namespace() -> "bridge".
|
||||||
|
|
||||||
api_spec() ->
|
api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false}).
|
emqx_dashboard_swagger:spec(?MODULE, #{
|
||||||
|
check_schema => fun emqx_dashboard_swagger:validate_content_type_json/2
|
||||||
|
}).
|
||||||
|
|
||||||
paths() ->
|
paths() ->
|
||||||
[
|
[
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
-export([base_path/0]).
|
-export([base_path/0]).
|
||||||
-export([relative_uri/1, get_relative_uri/1]).
|
-export([relative_uri/1, get_relative_uri/1]).
|
||||||
-export([compose_filters/2]).
|
-export([compose_filters/2]).
|
||||||
|
-export([validate_content_type_json/2, validate_content_type/3]).
|
||||||
|
|
||||||
-export([
|
-export([
|
||||||
filter_check_request/2,
|
filter_check_request/2,
|
||||||
|
@ -152,6 +153,30 @@ spec(Module, Options) ->
|
||||||
),
|
),
|
||||||
{ApiSpec, components(lists:usort(AllRefs), Options)}.
|
{ApiSpec, components(lists:usort(AllRefs), Options)}.
|
||||||
|
|
||||||
|
validate_content_type_json(Params, Meta) ->
|
||||||
|
validate_content_type(Params, Meta, <<"application/json">>).
|
||||||
|
|
||||||
|
%% tip: Skip content-type check if body is empty.
|
||||||
|
validate_content_type(
|
||||||
|
#{body := Body, headers := Headers} = Params,
|
||||||
|
#{method := Method},
|
||||||
|
Expect
|
||||||
|
) when
|
||||||
|
(Method =:= put orelse
|
||||||
|
Method =:= post orelse
|
||||||
|
method =:= patch) andalso
|
||||||
|
Body =/= #{}
|
||||||
|
->
|
||||||
|
ExpectSize = byte_size(Expect),
|
||||||
|
case maps:get(<<"content-type">>, Headers, undefined) of
|
||||||
|
<<Expect:ExpectSize/binary, _/binary>> ->
|
||||||
|
{ok, Params};
|
||||||
|
_ ->
|
||||||
|
{415, 'UNSUPPORTED_MEDIA_TYPE', <<"content-type:", Expect/binary, " Required">>}
|
||||||
|
end;
|
||||||
|
validate_content_type(Params, _Meta, _Expect) ->
|
||||||
|
{ok, Params}.
|
||||||
|
|
||||||
-spec namespace() -> hocon_schema:name().
|
-spec namespace() -> hocon_schema:name().
|
||||||
namespace() -> "public".
|
namespace() -> "public".
|
||||||
|
|
||||||
|
@ -341,8 +366,12 @@ translate_req(Request, #{module := Module, path := Path, method := Method}, Chec
|
||||||
Params = maps:get(parameters, Spec, []),
|
Params = maps:get(parameters, Spec, []),
|
||||||
Body = maps:get('requestBody', Spec, []),
|
Body = maps:get('requestBody', Spec, []),
|
||||||
{Bindings, QueryStr} = check_parameters(Request, Params, Module),
|
{Bindings, QueryStr} = check_parameters(Request, Params, Module),
|
||||||
NewBody = check_request_body(Request, Body, Module, CheckFun, hoconsc:is_schema(Body)),
|
case check_request_body(Request, Body, Module, CheckFun, hoconsc:is_schema(Body)) of
|
||||||
{ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}}
|
{ok, NewBody} ->
|
||||||
|
{ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}};
|
||||||
|
Error ->
|
||||||
|
Error
|
||||||
|
end
|
||||||
catch
|
catch
|
||||||
throw:HoconError ->
|
throw:HoconError ->
|
||||||
Msg = hocon_error_msg(HoconError),
|
Msg = hocon_error_msg(HoconError),
|
||||||
|
@ -523,6 +552,10 @@ unwrap_array_conv([HVal | _], _Opts) -> HVal;
|
||||||
unwrap_array_conv(SingleVal, _Opts) -> SingleVal.
|
unwrap_array_conv(SingleVal, _Opts) -> SingleVal.
|
||||||
|
|
||||||
check_request_body(#{body := Body}, Schema, Module, CheckFun, true) ->
|
check_request_body(#{body := Body}, Schema, Module, CheckFun, true) ->
|
||||||
|
%% the body was already being decoded
|
||||||
|
%% if the content-type header specified application/json.
|
||||||
|
case is_binary(Body) of
|
||||||
|
false ->
|
||||||
Type0 = hocon_schema:field_schema(Schema, type),
|
Type0 = hocon_schema:field_schema(Schema, type),
|
||||||
Type =
|
Type =
|
||||||
case Type0 of
|
case Type0 of
|
||||||
|
@ -532,7 +565,10 @@ check_request_body(#{body := Body}, Schema, Module, CheckFun, true) ->
|
||||||
NewSchema = ?INIT_SCHEMA#{roots => [{root, Type}]},
|
NewSchema = ?INIT_SCHEMA#{roots => [{root, Type}]},
|
||||||
Option = #{required => false},
|
Option = #{required => false},
|
||||||
#{<<"root">> := NewBody} = CheckFun(NewSchema, #{<<"root">> => Body}, Option),
|
#{<<"root">> := NewBody} = CheckFun(NewSchema, #{<<"root">> => Body}, Option),
|
||||||
NewBody;
|
{ok, NewBody};
|
||||||
|
true ->
|
||||||
|
{415, 'UNSUPPORTED_MEDIA_TYPE', <<"content-type:application/json Required">>}
|
||||||
|
end;
|
||||||
%% TODO not support nest object check yet, please use ref!
|
%% TODO not support nest object check yet, please use ref!
|
||||||
%% 'requestBody' = [ {per_page, mk(integer(), #{}},
|
%% 'requestBody' = [ {per_page, mk(integer(), #{}},
|
||||||
%% {nest_object, [
|
%% {nest_object, [
|
||||||
|
@ -541,6 +577,7 @@ check_request_body(#{body := Body}, Schema, Module, CheckFun, true) ->
|
||||||
%% ]}
|
%% ]}
|
||||||
%% ]
|
%% ]
|
||||||
check_request_body(#{body := Body}, Spec, _Module, CheckFun, false) when is_list(Spec) ->
|
check_request_body(#{body := Body}, Spec, _Module, CheckFun, false) when is_list(Spec) ->
|
||||||
|
{ok,
|
||||||
lists:foldl(
|
lists:foldl(
|
||||||
fun({Name, Type}, Acc) ->
|
fun({Name, Type}, Acc) ->
|
||||||
Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
|
Schema = ?INIT_SCHEMA#{roots => [{Name, Type}]},
|
||||||
|
@ -548,11 +585,11 @@ check_request_body(#{body := Body}, Spec, _Module, CheckFun, false) when is_list
|
||||||
end,
|
end,
|
||||||
#{},
|
#{},
|
||||||
Spec
|
Spec
|
||||||
);
|
)};
|
||||||
%% requestBody => #{content => #{ 'application/octet-stream' =>
|
%% requestBody => #{content => #{ 'application/octet-stream' =>
|
||||||
%% #{schema => #{ type => string, format => binary}}}
|
%% #{schema => #{ type => string, format => binary}}}
|
||||||
check_request_body(#{body := Body}, Spec, _Module, _CheckFun, false) when is_map(Spec) ->
|
check_request_body(#{body := Body}, Spec, _Module, _CheckFun, false) when is_map(Spec) ->
|
||||||
Body.
|
{ok, Body}.
|
||||||
|
|
||||||
%% tags, description, summary, security, deprecated
|
%% tags, description, summary, security, deprecated
|
||||||
meta_to_spec(Meta, Module, Options) ->
|
meta_to_spec(Meta, Module, Options) ->
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{application, emqx_dashboard_sso, [
|
{application, emqx_dashboard_sso, [
|
||||||
{description, "EMQX Dashboard Single Sign-On"},
|
{description, "EMQX Dashboard Single Sign-On"},
|
||||||
{vsn, "0.1.4"},
|
{vsn, "0.1.5"},
|
||||||
{registered, [emqx_dashboard_sso_sup]},
|
{registered, [emqx_dashboard_sso_sup]},
|
||||||
{applications, [
|
{applications, [
|
||||||
kernel,
|
kernel,
|
||||||
|
|
|
@ -38,7 +38,12 @@
|
||||||
namespace() -> "dashboard_sso".
|
namespace() -> "dashboard_sso".
|
||||||
|
|
||||||
api_spec() ->
|
api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false, translate_body => false}).
|
emqx_dashboard_swagger:spec(?MODULE, #{
|
||||||
|
translate_body => false,
|
||||||
|
check_schema => fun(Params, Meta) ->
|
||||||
|
emqx_dashboard_swagger:validate_content_type(Params, Meta, <<"application/xml">>)
|
||||||
|
end
|
||||||
|
}).
|
||||||
|
|
||||||
paths() ->
|
paths() ->
|
||||||
[
|
[
|
||||||
|
|
|
@ -28,7 +28,9 @@
|
||||||
namespace() -> "license_http_api".
|
namespace() -> "license_http_api".
|
||||||
|
|
||||||
api_spec() ->
|
api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false}).
|
emqx_dashboard_swagger:spec(?MODULE, #{
|
||||||
|
check_schema => fun emqx_dashboard_swagger:validate_content_type_json/2
|
||||||
|
}).
|
||||||
|
|
||||||
paths() ->
|
paths() ->
|
||||||
[
|
[
|
||||||
|
|
|
@ -216,6 +216,45 @@ t_create_failed(_Config) ->
|
||||||
?assertEqual(BadRequest, create_banned(Expired)),
|
?assertEqual(BadRequest, create_banned(Expired)),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
%% validate check_schema is true with bad content_type
|
||||||
|
t_create_with_bad_content_type(_Config) ->
|
||||||
|
Now = erlang:system_time(second),
|
||||||
|
At = emqx_banned:to_rfc3339(Now),
|
||||||
|
Until = emqx_banned:to_rfc3339(Now + 3),
|
||||||
|
Who = <<"TestClient-"/utf8>>,
|
||||||
|
By = <<"banned suite 中"/utf8>>,
|
||||||
|
Reason = <<"test测试"/utf8>>,
|
||||||
|
Banned = #{
|
||||||
|
as => clientid,
|
||||||
|
who => Who,
|
||||||
|
by => By,
|
||||||
|
reason => Reason,
|
||||||
|
at => At,
|
||||||
|
until => Until
|
||||||
|
},
|
||||||
|
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
||||||
|
Path = emqx_mgmt_api_test_util:api_path(["banned"]),
|
||||||
|
{error, {
|
||||||
|
{"HTTP/1.1", 415, "Unsupported Media Type"},
|
||||||
|
_Headers,
|
||||||
|
MsgBin
|
||||||
|
}} =
|
||||||
|
emqx_mgmt_api_test_util:request_api(
|
||||||
|
post,
|
||||||
|
Path,
|
||||||
|
"",
|
||||||
|
AuthHeader,
|
||||||
|
Banned,
|
||||||
|
#{'content-type' => "application/xml", return_all => true}
|
||||||
|
),
|
||||||
|
?assertEqual(
|
||||||
|
#{
|
||||||
|
<<"code">> => <<"UNSUPPORTED_MEDIA_TYPE">>,
|
||||||
|
<<"message">> => <<"content-type:application/json Required">>
|
||||||
|
},
|
||||||
|
emqx_utils_json:decode(MsgBin)
|
||||||
|
).
|
||||||
|
|
||||||
t_delete(_Config) ->
|
t_delete(_Config) ->
|
||||||
Now = erlang:system_time(second),
|
Now = erlang:system_time(second),
|
||||||
At = emqx_banned:to_rfc3339(Now),
|
At = emqx_banned:to_rfc3339(Now),
|
||||||
|
|
|
@ -118,6 +118,7 @@ request_api(Method, Url, QueryParams, AuthOrHeaders, Body, Opts) when
|
||||||
(Method =:= put) orelse
|
(Method =:= put) orelse
|
||||||
(Method =:= delete)
|
(Method =:= delete)
|
||||||
->
|
->
|
||||||
|
ContentType = maps:get('content-type', Opts, "application/json"),
|
||||||
NewUrl =
|
NewUrl =
|
||||||
case QueryParams of
|
case QueryParams of
|
||||||
"" -> Url;
|
"" -> Url;
|
||||||
|
@ -125,9 +126,8 @@ request_api(Method, Url, QueryParams, AuthOrHeaders, Body, Opts) when
|
||||||
end,
|
end,
|
||||||
do_request_api(
|
do_request_api(
|
||||||
Method,
|
Method,
|
||||||
{NewUrl, build_http_header(AuthOrHeaders), "application/json",
|
{NewUrl, build_http_header(AuthOrHeaders), ContentType, emqx_utils_json:encode(Body)},
|
||||||
emqx_utils_json:encode(Body)},
|
maps:remove('content-type', Opts)
|
||||||
Opts
|
|
||||||
).
|
).
|
||||||
|
|
||||||
do_request_api(Method, Request, Opts) ->
|
do_request_api(Method, Request, Opts) ->
|
||||||
|
|
|
@ -137,7 +137,9 @@ end).
|
||||||
namespace() -> "rule".
|
namespace() -> "rule".
|
||||||
|
|
||||||
api_spec() ->
|
api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false}).
|
emqx_dashboard_swagger:spec(?MODULE, #{
|
||||||
|
check_schema => fun emqx_dashboard_swagger:validate_content_type_json/2
|
||||||
|
}).
|
||||||
|
|
||||||
paths() ->
|
paths() ->
|
||||||
[
|
[
|
||||||
|
|
|
@ -60,8 +60,11 @@ maybe_json_decode(X) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
request(Method, Path, Params) ->
|
request(Method, Path, Params) ->
|
||||||
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
|
||||||
Opts = #{return_all => true},
|
Opts = #{return_all => true},
|
||||||
|
request(Method, Path, Params, Opts).
|
||||||
|
|
||||||
|
request(Method, Path, Params, Opts) ->
|
||||||
|
AuthHeader = emqx_mgmt_api_test_util:auth_header_(),
|
||||||
case emqx_mgmt_api_test_util:request_api(Method, Path, "", AuthHeader, Params, Opts) of
|
case emqx_mgmt_api_test_util:request_api(Method, Path, "", AuthHeader, Params, Opts) of
|
||||||
{ok, {Status, Headers, Body0}} ->
|
{ok, {Status, Headers, Body0}} ->
|
||||||
Body = maybe_json_decode(Body0),
|
Body = maybe_json_decode(Body0),
|
||||||
|
@ -386,6 +389,38 @@ t_rule_test_smoke(_Config) ->
|
||||||
?assertEqual([], FailedCases),
|
?assertEqual([], FailedCases),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
%% validate check_schema is function with bad content_type
|
||||||
|
t_rule_test_with_bad_content_type(_Config) ->
|
||||||
|
Params =
|
||||||
|
#{
|
||||||
|
<<"context">> =>
|
||||||
|
#{
|
||||||
|
<<"clientid">> => <<"c_emqx">>,
|
||||||
|
<<"event_type">> => <<"message_publish">>,
|
||||||
|
<<"payload">> => <<"{\"msg\": \"hello\"}">>,
|
||||||
|
<<"qos">> => 1,
|
||||||
|
<<"topic">> => <<"t/a">>,
|
||||||
|
<<"username">> => <<"u_emqx">>
|
||||||
|
},
|
||||||
|
<<"sql">> => <<"SELECT\n *\nFROM\n \"t/#\"">>
|
||||||
|
},
|
||||||
|
Method = post,
|
||||||
|
Path = emqx_mgmt_api_test_util:api_path(["rule_test"]),
|
||||||
|
Opts = #{return_all => true, 'content-type' => "application/xml"},
|
||||||
|
?assertMatch(
|
||||||
|
{error,
|
||||||
|
{
|
||||||
|
{"HTTP/1.1", 415, "Unsupported Media Type"},
|
||||||
|
_Headers,
|
||||||
|
#{
|
||||||
|
<<"code">> := <<"UNSUPPORTED_MEDIA_TYPE">>,
|
||||||
|
<<"message">> := <<"content-type:application/json Required">>
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
request(Method, Path, Params, Opts)
|
||||||
|
),
|
||||||
|
ok.
|
||||||
|
|
||||||
do_t_rule_test_smoke(#{input := Input, expected := #{code := ExpectedCode}} = Case) ->
|
do_t_rule_test_smoke(#{input := Input, expected := #{code := ExpectedCode}} = Case) ->
|
||||||
{_ErrOrOk, {{_, Code, _}, _, Body}} = sql_test_api(Input),
|
{_ErrOrOk, {{_, Code, _}, _, Body}} = sql_test_api(Input),
|
||||||
case Code =:= ExpectedCode of
|
case Code =:= ExpectedCode of
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Added validation and error handling to ensure requests with a JSON body include the required 'application/json' Content-Type header. If missing, the API now returns a 415 Unsupported Media Type status instead of a 400.
|
2
mix.exs
2
mix.exs
|
@ -58,7 +58,7 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
{:ekka, github: "emqx/ekka", tag: "0.19.3", override: true},
|
{:ekka, github: "emqx/ekka", tag: "0.19.3", override: true},
|
||||||
{:gen_rpc, github: "emqx/gen_rpc", tag: "3.3.1", override: true},
|
{:gen_rpc, github: "emqx/gen_rpc", tag: "3.3.1", override: true},
|
||||||
{:grpc, github: "emqx/grpc-erl", tag: "0.6.12", override: true},
|
{:grpc, github: "emqx/grpc-erl", tag: "0.6.12", override: true},
|
||||||
{:minirest, github: "emqx/minirest", tag: "1.4.0", override: true},
|
{:minirest, github: "emqx/minirest", tag: "1.4.1", override: true},
|
||||||
{:ecpool, github: "emqx/ecpool", tag: "0.5.7", override: true},
|
{:ecpool, github: "emqx/ecpool", tag: "0.5.7", override: true},
|
||||||
{:replayq, github: "emqx/replayq", tag: "0.3.8", override: true},
|
{:replayq, github: "emqx/replayq", tag: "0.3.8", override: true},
|
||||||
{:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true},
|
{:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true},
|
||||||
|
|
|
@ -86,7 +86,7 @@
|
||||||
{ekka, {git, "https://github.com/emqx/ekka", {tag, "0.19.3"}}},
|
{ekka, {git, "https://github.com/emqx/ekka", {tag, "0.19.3"}}},
|
||||||
{gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "3.3.1"}}},
|
{gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "3.3.1"}}},
|
||||||
{grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.12"}}},
|
{grpc, {git, "https://github.com/emqx/grpc-erl", {tag, "0.6.12"}}},
|
||||||
{minirest, {git, "https://github.com/emqx/minirest", {tag, "1.4.0"}}},
|
{minirest, {git, "https://github.com/emqx/minirest", {tag, "1.4.1"}}},
|
||||||
{ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.7"}}},
|
{ecpool, {git, "https://github.com/emqx/ecpool", {tag, "0.5.7"}}},
|
||||||
{replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.8"}}},
|
{replayq, {git, "https://github.com/emqx/replayq.git", {tag, "0.3.8"}}},
|
||||||
{pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}},
|
{pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}},
|
||||||
|
|
Loading…
Reference in New Issue