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

Review HttpClient Request header APIs #4808

Closed
sbordet opened this issue Apr 23, 2020 · 6 comments · Fixed by #4845
Closed

Review HttpClient Request header APIs #4808

sbordet opened this issue Apr 23, 2020 · 6 comments · Fixed by #4845

Comments

@sbordet
Copy link
Contributor

sbordet commented Apr 23, 2020

Jetty version
10.0.x

Description
In light of #4773 and #4777, the client Request.header() API should be reviewed.

Currently, there is only one API: header(String, String).
This has the problem that there is no semantic for add/put (i.e. add the header, or replace the header), there is no semantic for remove (you have to pass a null value), and it's not optimal if you already have the pre-computed HttpField.

So perhaps the API should just expose a HttpFields.Mutable to let application have all the needed semantic (without repeating them in Request) - however this has the problem that it will break the API fluency (since a Request.headers() method would return HttpFields.Mutable, not Request so fluent chaining won't be possible).

@gregw thoughts?

@sbordet
Copy link
Contributor Author

sbordet commented Apr 23, 2020

See also #577.

sbordet added a commit that referenced this issue May 4, 2020
Introduced:
* Request Request.headers(Consumer<HttpFields.Mutable>).
This allows applications to modify the headers, and chain calls.
It also delegates the precise semantic of put/add/remove/clear to HttpFields, so there is no API duplication.
* HttpRequest.header(HttpField) to efficiently add fields while normalizing the request (only used in implementation).

* HttpResponse.header(HttpField) to efficiently add fields while parsing the response (only used in implementation).
This pairs with HttpResponse.trailer(HttpField).
* HttpResponse.headers(Consumer<HttpFields.Mutable>) to modify the fields after they have been populated (only used in tests).

Removed:
* Request.[set,add,put,remove], replaced by headers(Consumer<HttpFields.Mutable>).

Deprecated:
* Request.header(String, String)
* Request.header(HttpHeader, String)
Both replaced by headers(Consumer<HttpFields.Mutable>) with clearer semantic for add/put/remove.

All the rest is code cleanup to remove the usage of the deprecated header() methods.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
sbordet added a commit that referenced this issue May 5, 2020
Fixed test failures.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
@joakime joakime linked a pull request May 6, 2020 that will close this issue
@joakime
Copy link
Contributor

joakime commented May 6, 2020

I was initially a bit nervous at the removal of some APIs, like Request.header(String, String) as it's one of the most commonly used API methods.

But the replacement just feels and looks better.

Old example

Request request = client.newRequest(uri)
    .path("/")
    .method(method);
if (clientClose)
    request.header(HttpHeader.CONNECTION, "close");
else if (serverClose)
    request.header("X-Close", "true");

New way

Request request = client.newRequest(uri)
    .path("/")
    .method(method)
    .headers((fields) -> {
        if (clientClose)
            fields.put(HttpHeader.CONNECTION, "close");
        else if (serverClose)
            fields.put("X-Close", "true");
    });

sbordet added a commit that referenced this issue May 8, 2020
Updates after review.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
sbordet added a commit that referenced this issue May 8, 2020
…ient_header_api

Fixes #4808 - Review HttpClient Request header APIs.
sbordet added a commit that referenced this issue Jul 13, 2020
For some reason, Request.getHeaders() returned HttpFields,
but HttpRequest.getHeaders() returned HttpFields.Mutable,
and it was obviously wrong.

Fixed WebSocket code that was relying on this API error.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
@chernser
Copy link

New API is counterintuitive and doesn't fit overall style. Then why query parameters still param(name, value) ?

@sbordet
Copy link
Contributor Author

sbordet commented Jun 13, 2024

Why it is counterintuitive? It gives you full access to the HttpFields API without duplicating all the semantic.

Then why query parameters still param(name, value) ?

Because the values require special encoding that 99.9% of the developers forget, so Jetty does it for them.

@chernser
Copy link

Why it is counterintuitive? It gives you full access to the HttpFields API without duplicating all the semantic.
I agree that previous API was may confusing, but part of it was quite useful.

It is counterintuitive because:

  • it doesn't follow the same pattern with other setters.
  • Consumer<> is usually used when consumer should process immutable object, Supplier<> is the closer alternative here.
  • Signature Consumer<Mutable> is not obvious - what is Mutable?

I'm would like has simple header(String name, String value) additionally to that so code would be more a chain like. Here is example:

 client.newRequest("http://localhost:8080")
  .method("GET") 
  .param("search", textSearch) 
  .header("X-app-log-metrics", "true")
  .header("X-app-client-id", clientId)
  .send()

vs

 client.newRequest("http://localhost:8080")
  .method("GET") 
  .param("search", textSearch) 
  .headers( (headers) -> {
       headers.put("X-app-log-metrics", "true")
       headers.put("X-app-client-id", clientId)
  })
  .send()

I think it is longer and more text to read. It "breaks" chain of common methods.

Don't get me wrong - I just would like an old way to be here too.

Because the values require special encoding that 99.9% of the developers forget, so Jetty does it for them.
Nothing stops to use Consumer<Params> and make params do the encoding. No-one I think would expect that client send it in as is.
Btw, if parameters have similar method than users may just pass method reference to, for example, add common parameters and headers. Please consider next example:

 
     client.newRequest("http://localhost:8080")
  .method("GET") 
  .param("search", textSearch) 
  .headers(CommonHeaders::add)
  .params(context::qparams)
  .send()

@sbordet
Copy link
Contributor Author

sbordet commented Jun 13, 2024

I'm would like has simple header(String name, String value) additionally to that so code would be more a chain like.

Read the first comment. header(String, String) is misleading.

We cannot support only super-simple examples, we have to support all use cases.

I don't understand your last example -- things seems more complicated and less clear just to be "chain like".

The goal is to provide clear and precise functionalities for all use cases, with little code duplication.

In your last example, it is not clear what happens to param("search", textSearch) if params(context::qparams) also provides a "search" parameter.
Is it replaced? Added? If added, what is the order? Throws an exception due to duplicate?

You may have a point about param(String, String) (same problems of header(String, String)) and to use Fields instead.
If you want to pursue that, please open a new issue.

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 a pull request may close this issue.

3 participants