Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add simple http client #1123

Open
wants to merge 9 commits into
base: release-0.6
Choose a base branch
from
Open

Add simple http client #1123

wants to merge 9 commits into from

Conversation

bettio
Copy link
Collaborator

@bettio bettio commented Mar 31, 2024

Add simple http client inspired to mint.

This kind of API works well with memory constrained devices, since it allows to handle chunks and connection events one by one.

TODO after this PR:

  • Rework default headers handling, such as Host and Content-Lenght
  • Handle chuncked body with delimiters

These changes are made under both the "Apache 2.0" and the "GNU Lesser General
Public License 2.1 or later" license terms (dual license).

SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later

@bettio bettio changed the base branch from main to release-0.6 March 31, 2024 13:56
@bettio bettio force-pushed the http-client branch 3 times, most recently from 0bb3026 to 6f6bdf3 Compare May 7, 2024 22:24
Copy link
Collaborator

@UncleGrumpy UncleGrumpy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about the format of your specs.

@bettio bettio force-pushed the http-client branch 2 times, most recently from 1e33d44 to 4470de7 Compare May 8, 2024 13:05
@UncleGrumpy
Copy link
Collaborator

UncleGrumpy commented May 8, 2024

That comment was about the function specs. Although it does apply to types as well. I did not get the first line highlighted and included in my comment so the context was not very clear.

@bettio bettio force-pushed the http-client branch 6 times, most recently from cb06fc9 to 1f23730 Compare May 13, 2024 21:15
@bettio bettio marked this pull request as ready for review May 13, 2024 21:16
Copy link
Collaborator

@UncleGrumpy UncleGrumpy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me.

Copy link
Contributor

@arpunk arpunk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this 🎉

Copy link
Collaborator

@pguyot pguyot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are lacking tests beyond the example. When trying to test this code beyond a simple redirect on atomvm.net (e.g. to get HTTP errors), I ended up realizing our ssl implementation is not compatible with TLS 1.3 by lack of proper verification options.

[{binary, true} | WithActive].

take_parse_headers(Options) ->
case proplists:get_value(parse_headers, Options) of
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should implement lists:keytake/3 to avoid parsing the list twice here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, opened #1159.


-define(DEFAULT_WANTED_HEADERS, [<<"Content-Length">>]).

-record(http_client, {host, socket, parser, ref, parse_headers}).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we please have types for record fields?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

%% @doc Makes a http request using given method on provided path.
%%
%% When using methods such as `GET' the body should be omited using either `undefined' or
%% `nil' (they are both equivalent).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have nil? If
undefined is not the only possible value, I would have expected [] or maybe none.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make it Elixir friendly. nil is like undefined but it has a special meaning in Elixir.

%% be provided.
%%
%% `stream' option can be used with `stream_request_body/3' in order to upload a bigger
%% binary in streaming mode. This option should be combined with `content-length' header.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't we stream without knowing the length? If it works we could just change the documentation with s/should be combined/may be combined/

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but it requires chunked uploads with delimiters, that is beyond the scope of this PR. I would work on this with a further set of improvements.

case send(Socket, Data) of
ok ->
Ref = make_ref(),
WantedHeaders = ?DEFAULT_WANTED_HEADERS ++ Conn#http_client.parse_headers,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may end up having content-length twice here if user wanted it. Also it's a little bit confusing, if I ask for a single header (e.g. Location), I end up having two. I don't need Content-Length if I am doing a HEAD request or I expect a Location.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, I think I need to improve this API on a further iteration.

split_header([Name | Tail], HeaderLine) ->
NameLen = byte_size(Name),
case HeaderLine of
<<Name:NameLen/binary, $:, Value/binary>> -> {ok, Name, Value};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a case issue here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

%%
%% The updated returned connection should be used in place of the previous one as a
%% parameter for the next call.
%% The returned reference can be used later for matching responses when using `stream/2'.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand this usage, and it's not apparent in example.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only place that I find the documentation a bit unclear too. Perhaps inserting a short example here would help clear up any confusion.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it can be used when handling multiple http requests from the same process, each request will have it's own reference.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to clarify it

Parser#parser_state{acc = Chunk}.

parse_line(
#parser_state{state = undefined} = Parser, <<"HTTP/1.0 ", C:3/binary, " ", _Txt/binary>>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't support HTTP 0.9 ? 😁

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to go into the investigating http 0.9 rabbit hole, but then I just realized that is past midnight... ☠️

libs/eavmlib/src/ahttp_client.erl Show resolved Hide resolved
transform_options(Options) ->
WithActive =
case proplists:get_value(active, Options) of
undefined -> [{active, true} | Options];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a workaround for ssl default, doesn't it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and no: we are not implementing the workaround here, but we are allowing it.
Applications using this API must implement the workaround on their side, this cannot be done in other ways.
Anyway supporting both active and passive mode is a good thing, and it should be kept even after the workaround.

%% @param Method a http method such as "GET", "POST", "PUT", etc...
%% @param Path the path to the http resource, such as "/"
%% @param Headers a list of headers
%% @param Body the body that is sent to the server, may be undefined or nil when there is no body
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also whether it is nil or none the exact term should probably be code syntax highlighted with an Erlang (`') code block in the @param definition, so that it is understood to be a atom, and not "empty" or zero... Unless you go with the [] option.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

%% The first returned response to a new request is a status `{status, Ref, 200}' for
%% a successful response. After that headers and data may follow.
%%
%% Since the response might span multiple socket messages, `stream/3' may be called
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be stream/2, or stream_request_body/3? I don't see a stream/3.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

TrimmedValue = trim_right_spaces(LTrimmedValue, byte_size(LTrimmedValue)),
UpdatedParser =
case Name of
<<"Content-Length">> ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Content-Length" is from the server right?

and thus parsing should be case insensitive?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

split_header was taking it from a list, so it was guaranteed to have some casing. I clarified it.

Add simple http client that can be used in different situations, such as
when performing updates, inspired to Mint Elixir http client.

Signed-off-by: Davide Bettio <davide@uninstall.it>
Signed-off-by: Davide Bettio <davide@uninstall.it>
Disable it since `ahttp_client` cannot be run right now.

Signed-off-by: Davide Bettio <davide@uninstall.it>
Signed-off-by: Davide Bettio <davide@uninstall.it>
@bettio bettio force-pushed the http-client branch 2 times, most recently from c51e040 to af0e539 Compare May 22, 2024 22:41
Signed-off-by: Davide Bettio <davide@uninstall.it>
Signed-off-by: Davide Bettio <davide@uninstall.it>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants