This is an automated email from the ASF dual-hosted git repository.
Jens-G pushed a commit to branch release/0.25.0
in repository https://gitbox.apache.org/repos/asf/thrift.git
The following commit(s) were added to refs/heads/release/0.25.0 by this push:
new 27fa70442 THRIFT-6366: Bound the message size in the Erlang binary and
compact protocols Client: erl
27fa70442 is described below
commit 27fa70442bccdf981d72b1e57d1080646cb5e360
Author: Jens Geyer <[email protected]>
AuthorDate: Thu Sep 24 00:15:38 2026 +0200
THRIFT-6366: Bound the message size in the Erlang binary and compact
protocols
Client: erl
thrift_binary_protocol and thrift_compact_protocol now read a message only
up to max_message_size: the option of new/2 or of the protocol factory, or
else the thrift application's max_message_size (100 MB by default) as it is
when the protocol is created.
Each read takes its bytes from what the message may still take, before they
are read, so a string whose declared length would take the message past the
maximum is refused on its length alone. message_begin sets that budget and
message_end clears it; between messages each read is held to the maximum on
its own. A refusal is {error, {message_size_exceeds_maximum, Max}}, which
message_begin now returns instead of failing a match.
thrift_client_util passes a max_message_size client option on to the
protocol, and the binary, compact and JSON protocol factories accept it.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
CHANGES.md | 2 +
lib/erl/include/thrift_constants.hrl | 9 +-
lib/erl/src/thrift_binary_protocol.erl | 102 +++++++---
lib/erl/src/thrift_client_util.erl | 3 +-
lib/erl/src/thrift_compact_protocol.erl | 74 +++++--
lib/erl/src/thrift_json_protocol.erl | 6 +-
lib/erl/test/test_thrift_max_message_size.erl | 273 ++++++++++++++++++++++++++
7 files changed, 419 insertions(+), 50 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index 3a7fb3824..1a9ef214a 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -601,6 +601,8 @@
- [THRIFT-6030](https://issues.apache.org/jira/browse/THRIFT-6030) - Harden
Erlang protocol negative sizes
- [THRIFT-6084](https://issues.apache.org/jira/browse/THRIFT-6084) - Add
byte-count limit to TCompactProtocol varint reader
+- [THRIFT-6366](https://issues.apache.org/jira/browse/THRIFT-6366) - Bound the
message size in the Erlang binary and compact protocols
+
### Go
diff --git a/lib/erl/include/thrift_constants.hrl
b/lib/erl/include/thrift_constants.hrl
index 0e46cd727..af55f22d1 100644
--- a/lib/erl/include/thrift_constants.hrl
+++ b/lib/erl/include/thrift_constants.hrl
@@ -75,8 +75,9 @@
%% bindings use. Override with {thrift, [{max_frame_size, Bytes}]}.
-define(DEFAULT_MAX_FRAME_SIZE, 16384000).
-%% The largest reply thrift_http_transport will read, and the largest message
-%% thrift_json_protocol will read. Same value the other bindings use for their
-%% maximum message size. Override with {thrift, [{max_message_size, Bytes}]},
-%% or for one transport or protocol with its max_message_size option.
+%% The largest message the binary, compact and JSON protocols will read, and
+%% the largest reply thrift_http_transport will read. Same value the other
+%% bindings use for their maximum message size. Override with
+%% {thrift, [{max_message_size, Bytes}]}, or for one transport or protocol with
+%% its max_message_size option.
-define(DEFAULT_MAX_MESSAGE_SIZE, (100 * 1024 * 1024)).
diff --git a/lib/erl/src/thrift_binary_protocol.erl
b/lib/erl/src/thrift_binary_protocol.erl
index e94f02521..b1cf57f6b 100644
--- a/lib/erl/src/thrift_binary_protocol.erl
+++ b/lib/erl/src/thrift_binary_protocol.erl
@@ -37,7 +37,11 @@
-record(binary_protocol, {
transport :: term(),
strict_read = true :: boolean(),
- strict_write = true :: boolean()
+ strict_write = true :: boolean(),
+ % the most a message may take; new/2 settles it
+ max_message_size :: pos_integer() | undefined,
+ % what the message being read may still take; undefined between messages
+ message_bytes_left :: non_neg_integer() | undefined
}).
-define(VERSION_MASK, 16#FFFF0000).
@@ -47,13 +51,26 @@
new(Transport) ->
new(Transport, _Options = []).
+%%--------------------------------------------------------------------
+%% Options include:
+%% {strict_read, Bool}, {strict_write, Bool}
+%% {max_message_size, Bytes} = The longest message to read, in place of the
+%% thrift application's max_message_size
+%%--------------------------------------------------------------------
new(Transport, Options) ->
State = #binary_protocol{transport = Transport},
State1 = parse_options(Options, State),
- thrift_protocol:new(?MODULE, State1).
+ thrift_protocol:new(?MODULE, State1#binary_protocol{max_message_size =
max_message_size(State1)}).
+
+max_message_size(#binary_protocol{max_message_size = undefined}) ->
+ application:get_env(thrift, max_message_size, ?DEFAULT_MAX_MESSAGE_SIZE);
+max_message_size(#binary_protocol{max_message_size = Max}) ->
+ Max.
parse_options([], State) ->
State;
+parse_options([{max_message_size, Max} | Rest], State) when is_integer(Max),
Max > 0 ->
+ parse_options(Rest, State#binary_protocol{max_message_size = Max});
parse_options([{strict_read, Bool} | Rest], State) when is_boolean(Bool) ->
parse_options(Rest, State#binary_protocol{strict_read = Bool});
parse_options([{strict_write, Bool} | Rest], State) when is_boolean(Bool) ->
@@ -164,19 +181,17 @@ write(This = #binary_protocol{transport = Trans}, Data) ->
%%
-read(This0, message_begin) ->
- {This1, Initial} = read(This0, ui32),
+read(This0 = #binary_protocol{max_message_size = Max}, message_begin) ->
+ {This1, Initial} = read(This0#binary_protocol{message_bytes_left = Max},
ui32),
case Initial of
{ok, Sz} when Sz band ?VERSION_MASK =:= ?VERSION_1 ->
%% we're at version 1
- {This2, {ok, Name}} = read(This1, string),
- {This3, {ok, SeqId}} = read(This2, i32),
- Type = Sz band ?TYPE_MASK,
- {This3, #protocol_message_begin{
- name = binary_to_list(Name),
- type = Type,
- seqid = SeqId
- }};
+ case read(This1, string) of
+ {This2, {ok, Name}} ->
+ read_message_begin(This2, Name, Sz band ?TYPE_MASK);
+ {This2, Error} ->
+ {This2, Error}
+ end;
{ok, Sz} when Sz < 0 ->
%% there's a version number but it's unexpected
{This1, {error, {bad_binary_protocol_version, Sz}}};
@@ -185,19 +200,20 @@ read(This0, message_begin) ->
{This1, {error, no_binary_protocol_version}};
{ok, Sz} when This1#binary_protocol.strict_read =:= false ->
%% strict_read is false, so just read the old way
- {This2, {ok, Name}} = read_data(This1, Sz),
- {This3, {ok, Type}} = read(This2, byte),
- {This4, {ok, SeqId}} = read(This3, i32),
- {This4, #protocol_message_begin{
- name = binary_to_list(Name),
- type = Type,
- seqid = SeqId
- }};
+ case read_data(This1, Sz) of
+ {This2, {ok, Name}} ->
+ case read(This2, byte) of
+ {This3, {ok, Type}} -> read_message_begin(This3, Name,
Type);
+ {This3, Error} -> {This3, Error}
+ end;
+ {This2, Error} ->
+ {This2, Error}
+ end;
Else ->
{This1, Else}
end;
read(This, message_end) ->
- {This, ok};
+ {This#binary_protocol{message_bytes_left = undefined}, ok};
read(This, struct_begin) ->
{This, ok};
read(This, struct_end) ->
@@ -304,23 +320,55 @@ read(This0, string) ->
if Sz < 0 -> error({protocol_error, negative_size}); true -> ok end,
read_data(This1, Sz).
+%% The sequence id that ends a message header, once its name and type are read.
+read_message_begin(This0, Name, Type) ->
+ case read(This0, i32) of
+ {This1, {ok, SeqId}} ->
+ {This1, #protocol_message_begin{
+ name = binary_to_list(Name),
+ type = Type,
+ seqid = SeqId
+ }};
+ {This1, Error} ->
+ {This1, Error}
+ end.
+
-spec read_data(#binary_protocol{}, non_neg_integer()) ->
{#binary_protocol{}, {ok, binary()} | {error, _Reason}}.
read_data(This, 0) ->
{This, {ok, <<>>}};
-read_data(This = #binary_protocol{transport = Trans}, Len) when
is_integer(Len) andalso Len > 0 ->
- {NewTransport, Result} = thrift_transport:read(Trans, Len),
- {This#binary_protocol{transport = NewTransport}, Result}.
+read_data(This0 = #binary_protocol{transport = Trans}, Len) when
is_integer(Len) andalso Len > 0 ->
+ case take(This0, Len) of
+ {ok, This1} ->
+ {NewTransport, Result} = thrift_transport:read(Trans, Len),
+ {This1#binary_protocol{transport = NewTransport}, Result};
+ Error ->
+ {This0, Error}
+ end.
+
+%% Takes Len bytes from what the message being read may still take, before
+%% they are read. Between messages, each read is held to the maximum on its
own.
+take(This = #binary_protocol{message_bytes_left = undefined, max_message_size
= Max}, Len) when
+ Len =< Max
+->
+ {ok, This};
+take(This = #binary_protocol{message_bytes_left = Left}, Len) when
is_integer(Left), Len =< Left ->
+ {ok, This#binary_protocol{message_bytes_left = Left - Len}};
+take(#binary_protocol{max_message_size = Max}, _Len) ->
+ {error, {message_size_exceeds_maximum, Max}}.
%%%% FACTORY GENERATION %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-record(tbp_opts, {
strict_read = true :: boolean(),
- strict_write = true :: boolean()
+ strict_write = true :: boolean(),
+ max_message_size :: pos_integer() | undefined
}).
parse_factory_options([], Opts) ->
Opts;
+parse_factory_options([{max_message_size, Max} | Rest], Opts) when
is_integer(Max), Max > 0 ->
+ parse_factory_options(Rest, Opts#tbp_opts{max_message_size = Max});
parse_factory_options([{strict_read, Bool} | Rest], Opts) when
is_boolean(Bool) ->
parse_factory_options(Rest, Opts#tbp_opts{strict_read = Bool});
parse_factory_options([{strict_write, Bool} | Rest], Opts) when
is_boolean(Bool) ->
@@ -337,6 +385,10 @@ new_protocol_factory(TransportFactory, Options) ->
[
{strict_read, ParsedOpts#tbp_opts.strict_read},
{strict_write, ParsedOpts#tbp_opts.strict_write}
+ | [
+ {max_message_size, Max}
+ || Max <- [ParsedOpts#tbp_opts.max_message_size], Max
=/= undefined
+ ]
]
);
{error, Error} ->
diff --git a/lib/erl/src/thrift_client_util.erl
b/lib/erl/src/thrift_client_util.erl
index 7c5f094be..447896c04 100644
--- a/lib/erl/src/thrift_client_util.erl
+++ b/lib/erl/src/thrift_client_util.erl
@@ -41,7 +41,8 @@ split_options([], ProtoIn, TransIn) ->
split_options([Opt = {OptKey, _} | Rest], ProtoIn, TransIn) when
OptKey =:= strict_read;
OptKey =:= strict_write;
- OptKey =:= protocol
+ OptKey =:= protocol;
+ OptKey =:= max_message_size
->
split_options(Rest, [Opt | ProtoIn], TransIn);
split_options([Opt = {OptKey, _} | Rest], ProtoIn, TransIn) when
diff --git a/lib/erl/src/thrift_compact_protocol.erl
b/lib/erl/src/thrift_compact_protocol.erl
index 5b3fec054..70464e1f6 100644
--- a/lib/erl/src/thrift_compact_protocol.erl
+++ b/lib/erl/src/thrift_compact_protocol.erl
@@ -46,7 +46,11 @@
read_stack = [] :: iodata(),
read_value = ?CBOOL_NONE :: cbool(),
write_stack = [] :: iodata(),
- write_id = ?ID_NONE :: non_neg_integer()
+ write_id = ?ID_NONE :: non_neg_integer(),
+ % the most a message may take; new/2 settles it
+ max_message_size :: pos_integer() | undefined,
+ % what the message being read may still take; undefined between messages
+ message_bytes_left :: non_neg_integer() | undefined
}).
-type t_compact() :: #t_compact{}.
@@ -91,10 +95,26 @@ cbool_to_bool(Value) -> Value =:= ?CBOOL_TRUE.
new(Transport) -> new(Transport, _Options = []).
-new(Transport, _Options) ->
- State = #t_compact{transport = Transport},
+%%--------------------------------------------------------------------
+%% Options include:
+%% {max_message_size, Bytes} = The longest message to read, in place of the
+%% thrift application's max_message_size
+%%--------------------------------------------------------------------
+new(Transport, Options) ->
+ Max = proplists:get_value(max_message_size,
max_message_size_options(Options)),
+ State = #t_compact{transport = Transport, max_message_size =
max_message_size(Max)},
thrift_protocol:new(?MODULE, State).
+max_message_size_options(Options) when is_list(Options) ->
+ [Opt || Opt = {max_message_size, Max} <- Options, is_integer(Max), Max >
0];
+max_message_size_options(_Options) ->
+ [].
+
+max_message_size(undefined) ->
+ application:get_env(thrift, max_message_size, ?DEFAULT_MAX_MESSAGE_SIZE);
+max_message_size(Max) ->
+ Max.
+
flush_transport(This = #t_compact{transport = Transport}) ->
{NewTransport, Result} = thrift_transport:flush(Transport),
{This#t_compact{transport = NewTransport}, Result}.
@@ -246,19 +266,23 @@ write(This = #t_compact{transport = Trans}, Data) ->
%%
%%
-read(This0, message_begin) ->
- {This1, {ok, ?PROTOCOL_ID}} = read(This0, ubyte),
+read(This0 = #t_compact{max_message_size = Max}, message_begin) ->
+ {This1, {ok, ?PROTOCOL_ID}} = read(This0#t_compact{message_bytes_left =
Max}, ubyte),
{This2, {ok, VerAndType}} = read(This1, ubyte),
?VERSION_1 = VerAndType band ?VERSION_MASK,
{This3, {ok, SeqId}} = read(This2, ui32),
- {This4, {ok, Name}} = read(This3, string),
- {This4, #protocol_message_begin{
- name = binary_to_list(Name),
- type = (VerAndType bsr ?TYPE_SHIFT_AMOUNT) band ?TYPE_BITS,
- seqid = SeqId
- }};
+ case read(This3, string) of
+ {This4, {ok, Name}} ->
+ {This4, #protocol_message_begin{
+ name = binary_to_list(Name),
+ type = (VerAndType bsr ?TYPE_SHIFT_AMOUNT) band ?TYPE_BITS,
+ seqid = SeqId
+ }};
+ {This4, Error} ->
+ {This4, Error}
+ end;
read(This, message_end) ->
- {This, ok};
+ {This#t_compact{message_bytes_left = undefined}, ok};
read(This = #t_compact{read_stack = Stack}, struct_begin) ->
{This#t_compact{read_stack = [0 | Stack]}, ok};
read(This = #t_compact{read_stack = [_H | T]}, struct_end) ->
@@ -381,20 +405,36 @@ read(This0, string) ->
{#t_compact{}, {ok, binary()} | {error, _Reason}}.
read_data(This, 0) ->
{This, {ok, <<>>}};
-read_data(This = #t_compact{transport = Trans}, Len) when is_integer(Len)
andalso Len > 0 ->
- {NewTransport, Result} = thrift_transport:read(Trans, Len),
- {This#t_compact{transport = NewTransport}, Result}.
+read_data(This0 = #t_compact{transport = Trans}, Len) when is_integer(Len)
andalso Len > 0 ->
+ case take(This0, Len) of
+ {ok, This1} ->
+ {NewTransport, Result} = thrift_transport:read(Trans, Len),
+ {This1#t_compact{transport = NewTransport}, Result};
+ Error ->
+ {This0, Error}
+ end.
+
+%% Takes Len bytes from what the message being read may still take, before
+%% they are read. Between messages, each read is held to the maximum on its
own.
+take(This = #t_compact{message_bytes_left = undefined, max_message_size =
Max}, Len) when
+ Len =< Max
+->
+ {ok, This};
+take(This = #t_compact{message_bytes_left = Left}, Len) when is_integer(Left),
Len =< Left ->
+ {ok, This#t_compact{message_bytes_left = Left - Len}};
+take(#t_compact{max_message_size = Max}, _Len) ->
+ {error, {message_size_exceeds_maximum, Max}}.
%%%% FACTORY GENERATION %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% returns a (fun() -> thrift_protocol())
-new_protocol_factory(TransportFactory, _Options) ->
+new_protocol_factory(TransportFactory, Options) ->
F = fun() ->
case TransportFactory() of
{ok, Transport} ->
thrift_compact_protocol:new(
Transport,
- []
+ max_message_size_options(Options)
);
{error, Error} ->
{error, Error}
diff --git a/lib/erl/src/thrift_json_protocol.erl
b/lib/erl/src/thrift_json_protocol.erl
index d2602949c..c5331db55 100644
--- a/lib/erl/src/thrift_json_protocol.erl
+++ b/lib/erl/src/thrift_json_protocol.erl
@@ -717,10 +717,10 @@ key_to_number(Key, Convert, Error) ->
%%%% FACTORY GENERATION %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% returns a (fun() -> thrift_protocol())
-new_protocol_factory(TransportFactory, _Options) ->
- % Only strice read/write are implemented
+new_protocol_factory(TransportFactory, Options) ->
+ MaxOptions = [Opt || is_list(Options), Opt = {max_message_size, _} <-
Options],
F = fun() ->
{ok, Transport} = TransportFactory(),
- thrift_json_protocol:new(Transport, [])
+ thrift_json_protocol:new(Transport, MaxOptions)
end,
{ok, F}.
diff --git a/lib/erl/test/test_thrift_max_message_size.erl
b/lib/erl/test/test_thrift_max_message_size.erl
new file mode 100644
index 000000000..d9364ab02
--- /dev/null
+++ b/lib/erl/test/test_thrift_max_message_size.erl
@@ -0,0 +1,273 @@
+%%
+%% Licensed to the Apache Software Foundation (ASF) under one
+%% or more contributor license agreements. See the NOTICE file
+%% distributed with this work for additional information
+%% regarding copyright ownership. The ASF licenses this file
+%% to you under the Apache License, Version 2.0 (the
+%% "License"); you may not use this file except in compliance
+%% with the License. You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing,
+%% software distributed under the License is distributed on an
+%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+%% KIND, either express or implied. See the License for the
+%% specific language governing permissions and limitations
+%% under the License.
+%%
+
+%% The binary and compact protocols read a message only up to its maximum
+%% size: the max_message_size option, or else the thrift application's
+%% max_message_size.
+-module(test_thrift_max_message_size).
+-include_lib("eunit/include/eunit.hrl").
+-include("thrift_constants.hrl").
+-include("thrift_protocol.hrl").
+
+%% thrift_socket_server handler callbacks
+-export([handle_function/2, handle_error/2]).
+
+-define(ARGS, {struct, [{1, string}]}).
+-define(MAX, 4096).
+
+%% {Name, Module, Options} of each protocol and header form under test.
+protocols() ->
+ [
+ {"binary", thrift_binary_protocol, []},
+ {"binary without version header", thrift_binary_protocol, [
+ {strict_read, false}, {strict_write, false}
+ ]},
+ {"compact", thrift_compact_protocol, []}
+ ].
+
+new(Module, Bytes, Options) ->
+ {ok, Transport} = thrift_membuffer_transport:new(Bytes),
+ {ok, Protocol} = Module:new(Transport, Options),
+ Protocol.
+
+%% What is left in the memory buffer under Protocol, or under the protocol's
+%% own state, which a failed read inside the protocol raises with.
+buffered({protocol, _Module, State}) ->
+ buffered(State);
+buffered(State) ->
+ {t_transport, thrift_membuffer_transport, {t_membuffer, Bytes}} =
element(2, State),
+ Bytes.
+
+%% The bytes of a call named Name whose one argument is the string Text.
+message(Module, Options, Name, Text) ->
+ Protocol0 = new(Module, <<>>, Options),
+ {Protocol1, ok} = thrift_protocol:write(Protocol0, #protocol_message_begin{
+ name = Name, type = ?tMessageType_CALL, seqid = 1
+ }),
+ {Protocol2, ok} = thrift_protocol:write(Protocol1, {?ARGS, {args, Text}}),
+ {Protocol3, ok} = thrift_protocol:write(Protocol2, message_end),
+ {Protocol4, ok} = thrift_protocol:flush_transport(Protocol3),
+ iolist_to_binary(buffered(Protocol4)).
+
+%% A call of exactly Size bytes, the argument making up the rest. The compact
+%% protocol writes the string's length in as few bytes as it needs, so the
+%% first guess is corrected once.
+message_of_size(Module, Options, Size) ->
+ Guess = Size - byte_size(message(Module, Options, "m", <<>>)),
+ First = message(Module, Options, "m", binary:copy(<<"a">>, Guess)),
+ Bytes = message(Module, Options, "m", binary:copy(<<"a">>, Guess -
(byte_size(First) - Size))),
+ Size = byte_size(Bytes),
+ Bytes.
+
+%% Reads one call as thrift_processor does: {Result, bytes taken from the
+%% transport, Protocol}. A refusal inside the arguments surfaces as the
+%% exception the struct reader raises for any failed read; where that
+%% exception does not carry the protocol, the bytes taken are unknown.
+read_call(Protocol0, Bytes) ->
+ try read_whole_call(Protocol0) of
+ {Outcome, Protocol} ->
+ {Outcome, byte_size(Bytes) - iolist_size(buffered(Protocol)),
Protocol}
+ catch
+ error:{badmatch, {Protocol, {error, _} = Error}} ->
+ {Error, byte_size(Bytes) - iolist_size(buffered(Protocol)),
Protocol};
+ error:{case_clause, {error, _} = Error} ->
+ {Error, unknown, undefined}
+ end.
+
+read_whole_call(Protocol0) ->
+ case thrift_protocol:read(Protocol0, message_begin) of
+ {Protocol1, #protocol_message_begin{name = Name}} ->
+ {Protocol2, {ok, Args}} = thrift_protocol:read(Protocol1, ?ARGS),
+ {Protocol3, ok} = thrift_protocol:read(Protocol2, message_end),
+ {{ok, Name, Args}, Protocol3};
+ {Protocol1, Error} ->
+ {Error, Protocol1}
+ end.
+
+refused() -> {error, {message_size_exceeds_maximum, ?MAX}}.
+
+%% A call up to the maximum is read, and one a byte longer is refused. A
+%% string that would take the call past the maximum is refused from its
+%% length, before its bytes are taken from the transport.
+limit_test_() ->
+ [
+ {Name,
+ ?_test(begin
+ Options = [{max_message_size, ?MAX} | Base],
+ AtMax = message_of_size(Module, Base, ?MAX),
+ ?assertMatch(
+ {{ok, "m", _}, ?MAX, _}, read_call(new(Module, AtMax,
Options), AtMax)
+ ),
+ ByOne = message_of_size(Module, Base, ?MAX + 1),
+ ?assertMatch({{error, _}, _, _}, read_call(new(Module, ByOne,
Options), ByOne)),
+ Long = message_of_size(Module, Base, 2 * ?MAX),
+ {Result, Taken, _} = read_call(new(Module, Long, Options),
Long),
+ ?assertEqual(refused(), Result),
+ ?assert(Taken < ?MAX)
+ end)}
+ || {Name, Module, Base} <- protocols()
+ ].
+
+%% A message name longer than the maximum is refused by message_begin, which
+%% reports it as an error rather than raising, and before its bytes are taken.
+name_test_() ->
+ [
+ {Name,
+ ?_test(begin
+ Bytes = message(Module, Base,
binary_to_list(binary:copy(<<"n">>, ?MAX)), <<>>),
+ Protocol = new(Module, Bytes, [{max_message_size, ?MAX} |
Base]),
+ {Protocol1, Result} = thrift_protocol:read(Protocol,
message_begin),
+ ?assertEqual(refused(), Result),
+ ?assert(byte_size(Bytes) - iolist_size(buffered(Protocol1)) =<
8)
+ end)}
+ || {Name, Module, Base} <- protocols()
+ ].
+
+%% Each message has the whole maximum to itself.
+one_message_after_another_test_() ->
+ [
+ {Name,
+ ?_test(begin
+ One = message_of_size(Module, Base, ?MAX),
+ Bytes = <<One/binary, One/binary, One/binary>>,
+ Protocol0 = new(Module, Bytes, [{max_message_size, ?MAX} |
Base]),
+ {{ok, _, _}, _, Protocol1} = read_call(Protocol0, Bytes),
+ {{ok, _, _}, _, Protocol2} = read_call(Protocol1, Bytes),
+ ?assertMatch({{ok, "m", _}, _, _}, read_call(Protocol2, Bytes))
+ end)}
+ || {Name, Module, Base} <- protocols()
+ ].
+
+%% Outside a message, reads are held to the maximum one at a time and do not
+%% add up: many structs read one after another without a message around them.
+outside_a_message_test_() ->
+ [
+ {Name,
+ ?_test(begin
+ Protocol0 = new(Module, <<>>, Base),
+ {Protocol1, ok} = thrift_protocol:write(
+ Protocol0, {?ARGS, {args, binary:copy(<<"s">>, 1000)}}
+ ),
+ One = iolist_to_binary(buffered(Protocol1)),
+ Bytes = binary:copy(One, 10),
+ Protocol2 = new(Module, Bytes, [{max_message_size, ?MAX} |
Base]),
+ Final = lists:foldl(
+ fun(_, P) ->
+ {P1, {ok, {Text}}} = thrift_protocol:read(P, ?ARGS),
+ ?assertEqual(1000, byte_size(Text)),
+ P1
+ end,
+ Protocol2,
+ lists:seq(1, 10)
+ ),
+ ?assertEqual(0, iolist_size(buffered(Final)))
+ end)}
+ || {Name, Module, Base} <- protocols()
+ ].
+
+%% Without the option, the thrift application's max_message_size applies; the
+%% option takes its place.
+application_env_test_() ->
+ {setup,
+ fun() ->
+ ok = load_thrift_application(),
+ Saved = application:get_env(thrift, max_message_size),
+ ok = application:set_env(thrift, max_message_size, ?MAX),
+ Saved
+ end,
+ fun restore_env/1, [
+ {Name,
+ ?_test(begin
+ Over = message_of_size(Module, Base, ?MAX + 1),
+ ?assertMatch({{error, _}, _, _}, read_call(new(Module,
Over, Base), Over)),
+ ?assertMatch(
+ {{ok, "m", _}, _, _},
+ read_call(new(Module, Over, [{max_message_size, 2 *
?MAX} | Base]), Over)
+ )
+ end)}
+ || {Name, Module, Base} <- protocols()
+ ]}.
+
+%%%% Over a socket %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+handle_function(_Function, Args) -> {reply, element(1, Args)}.
+
+handle_error(_Function, _Reason) -> ok.
+
+%% The server refuses a call over the application's maximum and closes that
+%% connection, and goes on serving others. A client given max_message_size
+%% refuses a reply over it.
+socket_test_() ->
+ {setup,
+ fun() ->
+ ok = load_thrift_application(),
+ Saved = application:get_env(thrift, max_message_size),
+ ok = application:set_env(thrift, max_message_size, 64 * 1024),
+ Saved
+ end,
+ fun restore_env/1, [
+ {atom_to_list(Protocol), ?_test(socket_calls(Port, Protocol))}
+ || {Port, Protocol} <- [{9110, binary}, {9111, compact}]
+ ]}.
+
+socket_calls(Port, Protocol) ->
+ {ok, Server} = thrift_socket_server:start([
+ {ip, "127.0.0.1"},
+ {port, Port},
+ {name, list_to_atom("max_message_size_server_" ++
integer_to_list(Port))},
+ {service, thrift_test_thrift},
+ {handler, ?MODULE},
+ {protocol, Protocol}
+ ]),
+ try
+ Options = [{protocol, Protocol}, {recv_timeout, 2000}],
+ {ok, Client0} = thrift_client_util:new("127.0.0.1", Port,
thrift_test_thrift, Options),
+ {Client1, Small} = thrift_client:call(Client0, testString,
[<<"small">>]),
+ ?assertEqual({ok, <<"small">>}, Small),
+ Large = binary:copy(<<"L">>, 128 * 1024),
+ ?assertMatch({_, {error, _}}, catch_call(Client1, testString,
[Large])),
+ {ok, Client2} = thrift_client_util:new("127.0.0.1", Port,
thrift_test_thrift, Options),
+ ?assertMatch(
+ {_, {ok, <<"again">>}}, thrift_client:call(Client2, testString,
[<<"again">>])
+ ),
+ {ok, Client3} = thrift_client_util:new(
+ "127.0.0.1", Port, thrift_test_thrift, [{max_message_size, 1024} |
Options]
+ ),
+ ?assertMatch({_, {error, _}}, catch_call(Client3, testString,
[binary:copy(<<"r">>, 2048)]))
+ after
+ thrift_socket_server:stop(Server)
+ end.
+
+%% A call whose connection fails or whose reply is refused, as {error, _}.
+catch_call(Client, Function, Args) ->
+ try thrift_client:call(Client, Function, Args) of
+ {Client1, {ok, Reply}} -> {Client1, {ok, Reply}};
+ {Client1, Other} -> {Client1, {error, Other}}
+ catch
+ Class:Reason -> {Client, {error, {Class, Reason}}}
+ end.
+
+load_thrift_application() ->
+ case application:load(thrift) of
+ ok -> ok;
+ {error, {already_loaded, thrift}} -> ok
+ end.
+
+restore_env(undefined) -> application:unset_env(thrift, max_message_size);
+restore_env({ok, Value}) -> application:set_env(thrift, max_message_size,
Value).