avaje-http icon indicating copy to clipboard operation
avaje-http copied to clipboard

[enhancement] setting cookies/headers/status platform-agnostic

Open mechite opened this issue 3 months ago • 15 comments

Avaje Jex has Context.Cookie which has a few methods like expires(ZonedDateTime), secure(boolean) etc. Javalin, etc has similar. If io.avaje.http.api had something like this as an API for setting cookies, which you can accept in a method as such:

@Controller
...
@Post
void test(Cookies cookies) {
    cookies.set("example", "value");
    cookies.set("example2", "value", Duration.ofDays(7));
}
...

then we can abstract this feature away and it would be the same for all generators, one less reason to inject Context, and makes the controllers slightly more portable between the underlying routing implementations.

I don't know if that is a goal or if this is within the scope of this project. Using Context, whether with constructor or method argument injection, pretty much works exactly the same way.

mechite avatar Aug 21 '25 05:08 mechite

Using Context, whether with constructor or method argument injection, pretty much works exactly the same way.

I mean either way you're injecting a class to set the cookies, so like what does this change?

SentryMan avatar Aug 22 '25 18:08 SentryMan

like what does this change?

Makes it agnostic/indepedent to the specific webserver target being Jex, Javalin, Helidon etc.

I think there are 2 such use cases that mean that the code needs to be specific to the webserver, one of those is setting Cookies and I think the other is a generic Response<T> that can contain content + headers + status code.

Adding those to the io.avaje.http.api would mean we don't then need to webserver specific code for those use cases.

It might be that the "setting Cookies" use case can also be supported via a Response<T>.

rbygrave avatar Aug 24 '25 19:08 rbygrave

So yes, JAX-RS ResponseBuilder supports setting cookies, reference https://docs.oracle.com/javaee/7/api/javax/ws/rs/core/Response.ResponseBuilder.html

edit: So far, at least for myself I've been building REST API's and so the need for a Response<T> hasn't been hit, but once we start building content/html apps then I think we are going to get requests for an equivalent to JAX-RS Response<T>

rbygrave avatar Aug 24 '25 20:08 rbygrave

but once we start building content/html apps then I think we are going to get requests for an equivalent to JAX-RS Response<T>

I'm not following, why would html apps cause this? You can provide the framework request/response objects as a parameter of the method so wouldn't people just do that?

SentryMan avatar Aug 24 '25 23:08 SentryMan

provide the framework request/response objects as a parameter of the method

You can totally do this yes.

so wouldn't people just do that?

(A) Because then that code is specific to the webserver (Jex, Helidon etc) making it more work to migrate if/when desired and generally making the devs work harder along the lines of doing their own templating integration etc.

(B) We already have HTMX support with JStachio etc ... so we have an abstraction that supports controlling the response body content but not the other aspects of the response programmatically. We can set response status via annotation [declaratively] but if we need to programmatically set status code, headers, cookies etc then unlike JAX-RS we don't have a "nice" way to do that currently or at least the approach is use the specific response of the webserver [which is ok'ish].

In a way, the io.avaje.http.api abstractions/annotations are really good for request but not so for response.

What we have is "OK", but yeah imo we are missing the equivalent of Response<T> ... and once we do "content" there is often the associated need for things like - last-modified, expires, etag, not modified etc.

rbygrave avatar Aug 25 '25 00:08 rbygrave

Because then that code is specific to the webserver (Jex, Helidon etc)

If we had more servers with differing API, this point would be more effective for me. Barring Helidon, the webservers we support in Avaje HTTP have almost identical API and are easily migrated. Thus to me the question is how often do you expect that people will be switch implementations to/from helidon?

if we need to programmatically set status code, headers, cookies etc then unlike JAX-RS we don't have a "nice" way to do that

Given that the underlying frameworks provide nice abstractions that do these things, I'm not feeling it. A programmatic abstraction over another very similar programmatic abstraction feels like a lateral move at best.

SentryMan avatar Aug 25 '25 03:08 SentryMan

If it is a goal to be as agnostic to routing implementations as possible, it is necessary, otherwise it is merely excess weight

Personally, I also am not convinced myself that people will be migrating between HTTP servers - but perhaps if we look at avaje-sigma, it could be nice being able to build the same API for a serverless environment and.. serverful one - say someone wanted to offer some on-prem service?

I can't think of many scenarios where it would happen, but if both JAX-RS & Spring have this abstracted... it just seems natural. I am neutral.


A random concept - I wonder if it would be possible to do exactly as Response<T>, but with just a response builder

Could reduce verbosity - and it allows you to make a Controller that implements any interface (that is, interfaces agnostic to the concept of a REST API), also without ever injecting Context/ServerResponse/etc.

@Controller
@Path("/widgets")
final class WidgetController {
  private final HelloComponent hello;
  WidgetController(HelloComponent hello) {
    this.hello = hello;
  }

  @Get("/{id}")
  Widget getById(int id) {
    if (id > 1000) {
        return Response.from(new Widget(id, "you really got it" + hello.hello()))
                .status(PRECONDITION_REQUIRED_428)
                .cookie("widgetToken", randomUUID().toString(), Duration.ofDays(7))
                .build();
        // uses custom exception to throw body, status code and response to WidgetController$Route
        // i don't know if this is a good idea?
    }
    return new Widget(id, "you got it" + hello.hello());
  }
}

(not sure if this 2nd example is convincing)

interface CustomerController {
  ...
  Customer getById(int id); // no HTTP-specific things here
}

@Controller
@Path("/customers")
class CustomerControllerImpl implements CustomerController {
  ...
  // can just use a response builder, which CustomerControllerImpl$Route can catch.
}

mechite avatar Aug 26 '25 03:08 mechite

Maybe I misunderstand the example, but imo it should be:

  @Get("/{id}")
  Response<Widget> getById(int id) {
    ...

... and now Response<Widget> can have status code, headers, cookies etc. If Widget is a html template / JStachio template the generated code can also deal with the templating aspect.

but with just a response builder

Yeah, very similar to JAX-RS Response.ResponseBuilder

If we didn't have any HTMX / JStachio support then we'd just not bother. Now that we have HTMX / JStachio support, then we hit these use cases where we want the templated content responses PLUS response headers etc.

The original description on this ticket doesn't really give us any idea of the original motivation / use case where it needs to set cookies, my htmx apps are currently internal only but if/when that changes I'll hit this need pretty quickly.

rbygrave avatar Aug 26 '25 04:08 rbygrave

if both JAX-RS & Spring have this abstracted... it just seems natural.

For me, the difference is that the microframeworks used by Avaje-HTTP already have high-level abstractions that make it easy and convenient to control the HTTP response fully. JAX-RS/Spring may have HttpServletResponse, but that is low-level and doesn't provide a convenient way of writing the response back. (Hence their need for another higher-level abstraction)

my htmx apps are currently internal only but if/when that changes I'll hit this need pretty quickly

I still don't understand the hesitation to use Context or how making your apps external-facing changes the situation. Do you anticipate that you'll be migrating your applications to a different framework?

SentryMan avatar Aug 26 '25 05:08 SentryMan

Maybe I misunderstand the example, but imo it should be:

Yeah, you did

to do exactly as Response<T>, but with just a response builder

My concept would allow you to avoid the generic return type definition, which you can then see the effect of in the 2nd example

// i don't know if this is a good idea?

mechite avatar Aug 26 '25 18:08 mechite

yeah you did. ... uses custom exception to throw body, status code and response to WidgetController$Route

Oh ok, so throwing an exception in order to catch it in WidgetController$Route - hmm [yeah, I totally missed that].

which you can then see the effect of in the 2nd example

Ok, I see. Hmm.

Hey @Mechite ... what was the use case that motivated this that desired setting cookies? Is is a REST API or something else like HTMX or something that is using templating etc?

how making your apps external-facing changes the situation

Once we go external with content type responses, we get a lot more interested in last-modified, expires, etag, not-modified etc. For internal only apps with low request/sec and maybe no http proxies and caching in play we don't care so much for these headers.

Do you anticipate that you'll be migrating your applications to a different framework?

Yes. HTMX apps migrating between Helidon and Jex and for myself I've already done one of these. This isn't big/difficult work at the moment at all but I think that once an app starts including last-modified, expires, etag with the JStachio/htmx use case it will grow be more of an issue.

rbygrave avatar Aug 26 '25 21:08 rbygrave

Yes. HTMX apps migrating between Helidon and Jex and for myself I've already done one of these. This isn't big/difficult work at the moment at all but I think that once an app starts including last-modified, expires, etag with the JStachio/htmx use case it will grow be more of an issue.

So is the main value proposition for you that it's easier to switch frameworks? Do you plan to continue switching frameworks after that migration?

Switching your application to all use the style of Response<T> would also be a migration. After doing that migration, would you also switch the underlying framework again?

Just trying to nail down the usefulness in your particular situation.

SentryMan avatar Aug 26 '25 21:08 SentryMan

what was the use case that motivated this that desired setting cookies? Is is a REST API

Yes - a REST API for a single-page app, using cookies ends up being the cleanest solution for token management, as we implement federated logins with external services etc, and customers will also need to be able to interface with the API themselves

I found that it made a much more transparent API that way.

something that is using templating etc?

SPA written with Vue, hosted serverless on CloudFlare Pages. On the backend side, it's just a transparent REST API


Specifically the idea of having this abstraction - it was just an idea that popped up, while I was moving from Javalin to Jex. I didn't have any trouble with that yet, migration was easy as find-and-replace for me

mechite avatar Aug 27 '25 00:08 mechite

Just leaving a comment to say I dislike my exception idea after thinking about it a bit. Some kind of thread local / ScopedValue etc makes more sense.

Personally I just want it to be possible on an API level to not have any HTTP-related type in the method signature Hence, I also prefer e.g. constructor-injecting Context, don't enjoy the way JAX-RS does it, etc.


If we don't do this feature, I'd like it if at least in the avaje docs, we enforce/suggest one way of working e.g. for Javalin, to avoid NotFoundResponse() etc, as Jex lacks those millions of classes (rightly so)

but even then, e.g. Heildon API differs enough, and this feature seems simple enough to implement.


Also, I wanted to make a Jex-like abstraction and associated avaje-http generator for HTTP3. QUIC runs on UDP, therefore you can and should run with the same certificate/keystore and port (say 443) as the TCP/HTTP2 server

If it could (theoretically) be possible to run two generators, you'd have this functionality for free. But once you inject a context, that falls out of the window

mechite avatar Nov 04 '25 10:11 mechite

Also, I wanted to make a Jex-like abstraction and associated avaje-http generator for HTTP3. QUIC runs on UDP, therefore you can and should run with the same certificate/keystore and port (say 443) as the TCP/HTTP2 server

mate if you know how to work http3 I'd sooner put it in jex directly. Remember that jex is an abstraction over the HttpServer facade, so if we can make whatever quic implementation implement the Httpserver API, we can just slide it into jex. I can handle upgrading connections from tcp to udp. I just lack the knowledge to implement quic.

SentryMan avatar Nov 04 '25 15:11 SentryMan