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

feat: add HTTPClient implementation using curl as a module #50964

Closed
wants to merge 1 commit into from

Conversation

jonbonazza
Copy link
Contributor

@jonbonazza jonbonazza commented Jul 28, 2021

This PR uses libcurl in multi mode (curl_multi) to implement the HTTPClient interface. It cleans up the implementations significantly and should reduce maintenance burden while giving us easier access to more features in the end.

fixes godotengine/godot-proposals#2903

Scope

The goal of this PR is only to create a curl-based implementation of HTTPClient that conforms to the existing interface and nothing more.

The net team has discussed several improvements to the HTTPClient interface in general that may be implemented in future PRs, but they remain outside the scope of this one.

Remaining Work

The current state of the PR is DRAFT. It still needs more testing and there are still a few remaining pieces left.

  • Support IPv6 DNS resolution using Godot's resolver APIs. I was unable to get this to work, but I consider it essential for this PR to be merged. IPv4 works fine though.
  • See if there's a way to make DNS resolution more performant by using the async apis. I'm not familiar enough with Godot's DNS APIs to know how best to use them, and the documentation for them is quite lacking. May want to just leave this up to a future improvement with its own PR.
  • Implement set_connection and get_connection. The way libcurl works doesn't lend itself well to the StreamPeer interface, so without some changes to that API, there's not a good way to do t his. If we do decide to use NetSocket as the socket implementation (see above item) then we get this for free, so that's one pro to that approach. That said, I don't see these functions used anywhere in the Godot codebase, so I'd question whether they are even needed to begin with.
  • Get libcurl building on all platforms. Currently, I only have it working on Windows. I know @Faless has gotten it working on Linux, but I'm not sure what was necessary there. This might take some effort. We should also take the time to decide what features to enable/disable to tune binary size and also add a flag for using the system libcurl if available.

If I've missed anything in either the implementation itself, or the TODO list above, please don't hesitate to bring it up so we can discuss and potentially address it.

Regarding Unit Tests

While we should certainly strive for as much unit tests coverage as possible, I didn't see any existing tests for the http client, so I've decided to leave that for an exercise for another day (and potentially another person).

I want to stress that my decision not to include unit tests is not to the discredit of the idea of unit tests in general; quite the contrary, in fact. I would like to see a day where Godot's unit test coverage nears 100% just as much as anyone. However the best way to test http code is by running it against a real HTTP server, and to my knowledge, Godot doesn't currently have the facilities to do that and I'm not comfortable convoluting this particular PR by adding those facilities.

A Note About The Commits

There's quite a lot of commits in this one and none of them are particularly clean. I'm a bit embarrassed about that, but it's mostly due to the fact I was learning so many new technologies along the way and ended up doing a lot of things wrong in a lot of different places in odd orders.

I suggest when merging this PR to squash all of the commits in order to keep things as clean as possible in master.

@Calinou
Copy link
Member

Calinou commented Jul 28, 2021

Currently, we only resolve one address for a domain. Godot's resolver APIs support resolving multiple

Wasn't this implemented by #49026?

We should also take the time to decide what features to enable/disable to tune binary size and also add a flag for using the system libcurl if available.

I would only enable HTTP support (both HTTP/1.x and HTTP/2). FTP support is rarely used nowadays.

However the best way to test http code is by running it against a real HTTP server, and to my knowledge, Godot doesn't currently have the facilities to do that and I'm not comfortable convoluting this particular PR by adding those facilities.

We can add something to CI that runs a Godot project configured to make HTTP requests against a resource known for its high uptime, such as https://httpbin.org. If any of the requests fail, exit the project with a non-zero exit code (get_tree().quit(1)). If we are worried about the web server being unavailable, we can host our own local Web server in GitHub Actions just before the project starts.

@Calinou Calinou added this to the 4.0 milestone Jul 28, 2021
@Faless
Copy link
Collaborator

Faless commented Jul 28, 2021

Few things also mentioned in the chat:

  • Only fetch the body during read_response_body_chunk. Not during poll ❗
  • For IP resolution, see:
    // Host contains hostname and needs to be resolved to IP
    resolving = IP::get_singleton()->resolve_hostname_queue_item(conn_host);
    status = STATUS_RESOLVING;

    case STATUS_RESOLVING: {
    ERR_FAIL_COND_V(resolving == IP::RESOLVER_INVALID_ID, ERR_BUG);
    IP::ResolverStatus rstatus = IP::get_singleton()->get_resolve_item_status(resolving);
    switch (rstatus) {
    case IP::RESOLVER_STATUS_WAITING:
  • [blocking] I'm not even really sure how this is expected to be used. Is it even needed?

When you want to handle the request in a thread.

  • Implement set_connection and get_connection.

If we don't use our NetSocket implementation, just have them do nothing/return null and spit an error. Like JavaScript does:

void HTTPClientJavaScript::set_connection(const Ref<StreamPeer> &p_connection) {
ERR_FAIL_MSG("Accessing an HTTPClientJavaScript's StreamPeer is not supported for the HTML5 platform.");
}

  • Docs... I dread this one.

Well, be happy, you won't have to write any new doc, given you are creating a new implementation of an exposed class, not a new exposed class (so HTTPClient is already fully documented).
You can of course try to improved the documentation, if you like :).

  • Get libcurl building on all platforms.

Yeah, this is going to be quite annoying, on Linux I only got it to work with system-wide mbedtls (not with built-in) and using @fire curl_config.h.

@Faless
Copy link
Collaborator

Faless commented Jul 28, 2021

While we should certainly strive for as much unit tests coverage as possible, I didn't see any existing tests for the http client, so I've decided to leave that for an exercise for another day (and potentially another person).

I had testing for HTTPClient on a separate repository like the one you are proposing (those are not unit tests):
https://github.com/Faless/gd-tests/

We can use those to verify the correct behaviour, the main problem was that websites would, from time to time slightly change their layout, so of course tests would unexpectedly break.

@Faless
Copy link
Collaborator

Faless commented Jul 28, 2021

Size comparison (platform=linuxbsd target=release builtin_mbedtls=no):
Curl: 53436 KiB
NoCurl: 53176 KiB
Diff: ~260 KiB (could probably save some more with LTO)

@jonbonazza
Copy link
Contributor Author

Currently, we only resolve one address for a domain. Godot's resolver APIs support resolving multiple

Wasn't this implemented by #49026?

Yes, the engine supports it. This code, however does not. I am also unsure whether it's possible to do in an async manner. It's also worth mentioning that the current HTTPClient implementation does not seem to do multi address resolution, so Im gonna leave it to be outside the scope of this PR for now.

@jonbonazza
Copy link
Contributor Author

Okay I've trimmed down the TODO list a bit.

Only fetch the body during read_response_body_chunk. Not during poll ❗

Fixed this.

Also, @Faless I've updated the code to use the async resolver polling. It's basically the same code as the existing HTTPClientTCP you referenced, so nothing too crazy there.

Still can't get IPv6 to work and still not quite groking the blocking mode stuff. I've left a couple messages in the dev chat.

@jonbonazza jonbonazza marked this pull request as ready for review August 29, 2021 05:28
@jonbonazza jonbonazza requested review from a team as code owners August 29, 2021 05:28
@jonbonazza
Copy link
Contributor Author

jonbonazza commented Aug 29, 2021

This is ready for review.

I've tested this on Windows using the attached project, however it should probably be tested on linux, osx, android, and ios as well for posterity.

curl_test.zip

@YuriSizov YuriSizov requested review from a team and removed request for a team August 29, 2021 10:41
@Faless
Copy link
Collaborator

Faless commented Aug 29, 2021

Would be great to squash, leaving 2 commits, one adding the third-party library the other adding the module itself.

@fire
Copy link
Member

fire commented Aug 29, 2021

@jonbonazza Will work on rebasing into two prs and resolving the reviews.

@fire fire force-pushed the curl branch 2 times, most recently from d028cdd to 8546adb Compare August 29, 2021 18:20
@YuriSizov YuriSizov removed the request for review from a team August 29, 2021 18:29
@fire
Copy link
Member

fire commented Aug 30, 2021

Historical note: The module name mbedtls_curl was done to force this module to build after mbedtls. If it is possible to rename to curl, it would be better.

@fire
Copy link
Member

fire commented Aug 30, 2021

In file included from thirdparty/curl/lib/vtls/mbedtls.c:59:
thirdparty/curl/lib/sendf.h:37:2: error: "missing VARIADIC macro define, fix and rebuild!"
#error "missing VARIADIC macro define, fix and rebuild!"
 ^
Compiling ==> thirdparty/curl/lib/bufref.c
1 error generated.
scons: *** [thirdparty/curl/lib/vtls/mbedtls.windows.opt.tools.64.o] Error 1
In file included from thirdparty/curl/lib/altsvc.c:35:
thirdparty/curl/lib/sendf.h:37:2: error: "missing VARIADIC macro define, fix and rebuild!"
#error "missing VARIADIC macro define, fix and rebuild!"
 ^
1 error generated.
scons: *** [thirdparty/curl/lib/altsvc.windows.opt.tools.64.o] Error 1
In file included from thirdparty/curl/lib/asyn-thread.c:67:
thirdparty/curl/lib/sendf.h:37:2: error: "missing VARIADIC macro define, fix and rebuild!"
#error "missing VARIADIC macro define, fix and rebuild!"
 ^
1 error generated.
scons: *** [thirdparty/curl/lib/asyn-thread.windows.opt.tools.64.o] Error 1
In file included from thirdparty/curl/lib/vtls/vtls.c:55:
thirdparty/curl/lib/sendf.h:37:2: error: "missing VARIADIC macro define, fix and rebuild!"
#error "missing VARIADIC macro define, fix and rebuild!"
 ^
1 error generated.
scons: *** [thirdparty/curl/lib/vtls/vtls.windows.opt.tools.64.o] Error 1

Using mingw llvm.

/bin/bash -c "PATH=/opt/llvm-mingw/bin:$PATH scons werror=no platform=windows target=release_debug -j`nproc` use_lto=no deprecated=no use_mingw=yes use_llvm=yes use_lld=yes use_thinlto=yes LINKFLAGS=-Wl,-pdb= CCFLAGS='-g -gcodeview' debug_symbols=no custom_modules=../godot_custom_modules"

@jonbonazza
Copy link
Contributor Author

@fire I've reduced the commits down to 2.

@jonbonazza jonbonazza force-pushed the curl branch 2 times, most recently from 9ae14fa to c918d7e Compare December 7, 2021 20:49
@Faless
Copy link
Collaborator

Faless commented Jan 12, 2022

It doesn't so handle HEAD requests correctly:

@onready
var req = $HTTPRequest

# Called when the node enters the scene tree for the first time.
func _ready():
        req.connect("request_completed", Callable(self, "_completed"))
        # Stalls instead of completing after the header like the current implementation does.
        req.request("https://google.com/", [], false, HTTPClient.METHOD_HEAD)

proj_httprequest_head.zip

Please refer to https://github.com/Faless/gd-tests/ (quite outdated) for some test URL.

Please test at least:

  • GET/HEAD/POST both w/ w/o SSL, both w/ w/o known content length.
  • Chunked/Content-Length keep-alive.
  • Read-till-EOF.

Would be better to also test:

  • PUT/DELETE (very common in rest APIs)
  • OPTIONS (not sure, but should be supported by the current implementation, for CORS pre-flight).

@jonbonazza jonbonazza force-pushed the curl branch 5 times, most recently from e0567f3 to 5f019e6 Compare January 27, 2022 09:32
@jonbonazza
Copy link
Contributor Author

@Faless okay, I've tested every HTTP method I could think of and both chunked and not chunked for the ones that made sense. I also went through all the tests in your test project and made sure my responses matched what the tests expected. I also added all of the default headers that the old http client was adding.

One thing is I did screw up the git history so I ended up just squashing everything down.

@jonbonazza
Copy link
Contributor Author

FWIW, a suuuper handy tool for doing these tests was https://httpbin.org

@mhilbrunner
Copy link
Member

mhilbrunner commented Jan 27, 2022

I just want to chime in and say that its awesome how consistently you've been working away on this. Thanks for the efforts!

@jonbonazza
Copy link
Contributor Author

More bugs were founds. Squashing. 🐞

Co-authored-by: Jon Bonazza <jonbonazza@gmail.com>
Co-authored-by: Fabio Alessandrelli <fabio.alessandrelli@gmail.com>
@jonbonazza
Copy link
Contributor Author

Ooookay. Squashed all the bugs I found. @fire the issue you found should also be resolved.

@fire
Copy link
Member

fire commented Jan 27, 2022

I can still see a bug. Having trouble reducing a test case.

@akien-mga akien-mga modified the milestones: 4.0, 4.x Jan 19, 2023
@int-72h
Copy link

int-72h commented Mar 1, 2023

This feature looks great (as at the moment I'm having to use a GDNative extension), will it be merged anytime soon?

@andy5995
Copy link

This feature looks great (as at the moment I'm having to use a GDNative extension), will it be merged anytime soon?

Looks like it has merge conflicts at the moment.

@fire
Copy link
Member

fire commented Mar 20, 2023

This feature is abandoned.

@mhilbrunner
Copy link
Member

Closing after discussion at the Valencia sprint - besides this being obviously abandoned now, this was more difficult than anticipated, the upsides were smaller than hoped, and all in all the networking team now feels this is firmly in GDExtension territory for now.

Huge thanks to jonbonazza for exploring this and spending so much time on the implementation. (And of course to everyone reviewing :))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add HTTP client implementation using libcurl as a module, optionally replacing the default one
9 participants