recovered webclient branch; needs a little clean up#2771
Conversation
jurgenvinju
commented
May 12, 2026
- added initial webclient API that mirrors the Webserver API and uses the same Response and Request encodings
- cleanup and added POST method
- added the other methods
- added progress bar
- fixed post
…he same Response and Request encodings
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2771 +/- ##
========================================
- Coverage 46% 46% -1%
Complexity 6728 6728
========================================
Files 794 795 +1
Lines 65936 66120 +184
Branches 9888 9900 +12
========================================
- Hits 30860 30854 -6
- Misses 32693 32874 +181
- Partials 2383 2392 +9 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
DavyLandman
left a comment
There was a problem hiding this comment.
I think this is a nice idea but not ready yet, I've written down my concerns.
I think this needs tests.
| private HttpRequest makeGetRequest(IConstructor input) { | ||
| var params = input.asWithKeywordParameters(); | ||
| var host = ((ISourceLocation) params.getParameter("host")); | ||
| host = host == null ? URIUtil.assumeCorrectLocation("http://www.example.com") : host; |
There was a problem hiding this comment.
I think a missing host should not default to a request against http://www.example.com but instead throw an exception.
what if the scheme is not http/https?
There was a problem hiding this comment.
If the scheme is not https then the host will not be resolved and you get the appropriate http error code.
There was a problem hiding this comment.
okay, but the fallback to example.com should not be there, but it should throw an exception for a missing host.
|
|
||
| Thread writer = new Thread(() -> { | ||
| try (OutputStream os = out; Writer w = new OutputStreamWriter(out)) { | ||
| JsonWriter jsonWriter = new JsonWriter(w); |
There was a problem hiding this comment.
not all POST request have JSON bodies, we should think of a design that doesn't enforce json, but allows the user to specify the body format.
There was a problem hiding this comment.
Yes that's possible. The mirror design in response body's is that we have text with arbitrary mime types, files with mime types decided by file extension and JSON values as arbitrary serialized rascal values. That seems to be general enough.
| PipedOutputStream out = new PipedOutputStream(); | ||
| PipedInputStream in = new PipedInputStream(out, 64 * 1024); | ||
|
|
||
| Thread writer = new Thread(() -> { |
There was a problem hiding this comment.
I don't really like this pattern of starting a separate thread just to invert the stream direction. Looking at the BodyPublishers, it should be okayish to write a class that wraps a ByteBuffer around a buffer that's getting flushed.
Here is such an implementation:
public class OutputStreamBodySupplier extends BufferedOutputStream implements BodyPublisher {
private final List<Subscriber<? super ByteBuffer>> subscribers;
public OutputStreamBodySupplier() {
super(new PublishingStream());
this.subscribers = ((PublishingStream)super.out).subscribers;
}
/**
* The buffed outputstream will take care to collect the bytes untill there's a decent chunk to forward to the consumers
*/
private static class PublishingStream extends OutputStream {
private final List<Subscriber<? super ByteBuffer>> subscribers = new CopyOnWriteArrayList<>();
@Override
public void write(int b) throws IOException {
for (var sub: subscribers) {
sub.onNext(ByteBuffer.wrap(new byte[] { (byte)(b & 0xFF) }));
}
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
for (var sub: subscribers) {
sub.onNext(ByteBuffer.wrap(b, off, len).asReadOnlyBuffer());
}
}
@Override
public void close() throws IOException {
for (var sub: subscribers) {
sub.onComplete();
}
}
}
@Override
public void subscribe(Subscriber<? super ByteBuffer> subscriber) {
this.subscribers.add(subscriber);
}
@Override
public long contentLength() {
return -1;
}
}There was a problem hiding this comment.
Me too. Thanks for the code!
| PipedInputStream in = new PipedInputStream(out, 64 * 1024); | ||
|
|
||
| Thread writer = new Thread(() -> { | ||
| try (OutputStream os = out; Writer w = new OutputStreamWriter(out)) { |
There was a problem hiding this comment.
the encoding should be set for the writer, and the same encoding should also be set in the http headers.
nit: the writer will close os/out no need to add that to the try with resources.
| jsonOut.write(jsonWriter, val); | ||
| } | ||
| catch (Exception e) { | ||
| throw RuntimeExceptionFactory.io(e.getMessage()); |
There was a problem hiding this comment.
this exception will never pop up in rascal, as it's running on a separate thread.
There was a problem hiding this comment.
That's another negative aspect of this thread loop indeed. Let's get rid of it.
| | unsupportedMediaType() | ||
| ; | ||
|
|
||
| data Request( |
There was a problem hiding this comment.
I'm missing the option to set headers in the request, quite some API's require this.
There was a problem hiding this comment.
That's provided in the Content module. Here you see only the things we need extra to make the client api work. The goal is to make Request and Response be shared as much as possible between server and client API.
There was a problem hiding this comment.
okay, but I'm also missing that mapping in the java side. that's why I remarked that here.
|
|
||
| @synopsis{Short-hand for construction of JSON post bodies} | ||
| Request jsonPost(str path, &T content, loc host=|http://www.example.com|, BodyExpectation body = textBody()) | ||
| = post(path, &T (type[&T] _expected) { return content; }, host=host, body=body); |
There was a problem hiding this comment.
Shouldn't this also change the content type to application/json?
There was a problem hiding this comment.
Yes and also there are missing parameters for the headers, query, etc.
| .build(); | ||
| } | ||
|
|
||
| private HttpRequest makePutRequest(IConstructor input) { |
There was a problem hiding this comment.
That should be one of the header settings in the keyword field of this constructor. I'll check.
| }); | ||
|
|
||
| writer.start(); | ||
|
|
| Type respCons; | ||
| IString body; | ||
|
|
||
| switch (expect != null ? expect.getName() : "textBody") { |
There was a problem hiding this comment.
I'm a bit surprised, from the Webclient.rsc I got the expression the ExpectedBody was about the body of the http request. but here it looks like it's about the expected type of the response? I think that is not well aligned in the naming and the feature of the post implementation.
There was a problem hiding this comment.
It is for all requests, get, put, post, etc. When the server sends a response it has the three options: text, file or JSON (serialized from any rascal value). Here the client mirrors that design exactly where it can receive text, a file or json (parsed to a rascal value).
See above: posts also have bodies, but there you get the server perspective/direction so I'm inclined to see if we can factor the common parts. BodyProviders and BodyConsumers.
This will break backward compatibility for post in webserver so I wanted to push this to a later PR.
|
Great review @DavyLandman I'll try and improve this PR and let's have a look together later again. I've tested this client so far with a couple of web APIs and some very large test downloads. It seems very natural apart from the host/path/query separation. I think some shorthands where a full URL can be used which is split up internally would be nice. Also there is no support for fragments yet. |


