jetty / jetty.project

Eclipse Jetty® - Web Container & Clients - supports HTTP/2, HTTP/1.1, HTTP/1.0, websocket, servlets, and more
https://eclipse.dev/jetty
Other
3.87k stars 1.91k forks source link

java.io.IOException: written 4677 > 0 content-length (Jetty 11 to 12 upgrade) #12541

Closed rorytorneymf closed 1 week ago

rorytorneymf commented 1 week ago

Jetty Version 12.0.14

Jetty Environment ee10

Java Version JRE 21

Question After upgrading my app from Jetty 11.0.24 to Jetty 12.0.14, I am getting the following error when Jetty tries to send a response. It looks like the Content-Length header is getting set to 0, when there are > 0 bytes being written:

o.e.j.ee10.servlet.HttpOutput: {"message":"onWriteComplete(true,java.io.IOException: written 4677 > 0 content-length) s=CLOSING,api=BLOCKED,sc=false,e=null->s=CLOSED,api=BLOCKING,sc=false,e=null c=null cb=Callback@32b98b0b{NON_BLOCKING, org.eclipse.jetty.ee10.servlet.ServletChannel$$Lambda/0x00007fac6f69ada0@3ebbfb77,org.eclipse.jetty.ee10.servlet.ServletChannel$$Lambda/0x00007fac6f69afb8@3bbbe09c} w=false","exception":"java.io.IOException: written 4677 > 0 content-length\n\tat org.eclipse.jetty.server.internal.HttpChannelState$ChannelResponse.write(HttpChannelState.java:1271)\n\tat org.eclipse.jetty.server.Response$Wrapper.write(Response.java:768)\n\tat org.eclipse.jetty.server.handler.gzip.GzipResponseAndCallback.write(GzipResponseAndCallback.java:133)\n\tat org.eclipse.jetty.server.Response$Wrapper.write(Response.java:768)\n\tat org.eclipse.jetty.server.handler.ContextResponse.write(ContextResponse.java:56)\n\tat org.eclipse.jetty.ee10.servlet.ServletContextResponse.write(ServletContextResponse.java:288)\n\tat org.eclipse.jetty.ee10.servlet.HttpOutput.channelWrite(HttpOutput.java:206)\n\tat org.eclipse.jetty.ee10.servlet.HttpOutput.complete(HttpOutput.java:437)\n\tat org.eclipse.jetty.ee10.servlet.ServletContextResponse.completeOutput(ServletContextResponse.java:212)\n\tat org.eclipse.jetty.ee10.servlet.ServletChannel.handle(ServletChannel.java:576)\n\tat org.eclipse.jetty.ee10.servlet.ServletHandler.handle(ServletHandler.java:464)\n\tat org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:575)\n\tat org.eclipse.jetty.ee10.servlet.SessionHandler.handle(SessionHandler.java:717)\n\tat org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:1060)\n\tat org.eclipse.jetty.server.Handler$Wrapper.handle(Handler.java:740)\n\tat io.dropwizard.metrics.jetty12.AbstractInstrumentedHandler.handle(AbstractInstrumentedHandler.java:299)\n\tat io.dropwizard.jetty.RoutingHandler.handle(RoutingHandler.java:41)\n\tat org.eclipse.jetty.server.handler.gzip.GzipHandler.handle(GzipHandler.java:597)\n\tat io.dropwizard.jetty.ZipExceptionHandlingGzipHandler.handle(ZipExceptionHandlingGzipHandler.java:21)\n\tat org.eclipse.jetty.server.Handler$Wrapper.handle(Handler.java:740)\n\tat org.eclipse.jetty.server.handler.GracefulHandler.handle(GracefulHandler.java:101)\n\tat org.eclipse.jetty.server.Server.handle(Server.java:182)\n\tat org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:662)\n\tat org.eclipse.jetty.server.internal.HttpConnection.onFillable(HttpConnection.java:414)\n\tat org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:322)\n\tat org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99)\n\tat org.eclipse.jetty.io.ssl.SslConnection$SslEndPoint.onFillable(SslConnection.java:575)\n\tat org.eclipse.jetty.io.ssl.SslConnection.onFillable(SslConnection.java:390)\n\tat org.eclipse.jetty.io.ssl.SslConnection$2.succeeded(SslConnection.java:150)\n\tat org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99)\n\tat org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)\n\tat org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:979)\n\tat org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1209)\n\tat org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1164)\n\tat java.base/java.lang.Thread.run(Thread.java:1583)\n"}

I disabled GZIP to determine if it was a GZIP-specific issue, but still got the same error.

Jetty 11 logs (working):

c.m.a.g.SecureHeaderFilter: response headers [CAF-Correlation-Id: cf67034c-1362-4781-8b50-c5ba49505430, X-Frame-Options: SAMEORIGIN, X-Robots-Tag: none, Strict-Transport-Security: max-age=31536000; includeSubDomains, X-Content-Type-Options: nosniff, Content-Security-Policy: frame-src 'self'; frame-ancestors 'self'; object-src 'none';, Vary: Origin, X-XSS-Protection: 1; mode=block, Date: Fri, 15 Nov 2024 12:40:15 GMT]

o.e.jetty.server.HttpOutput: write(array HeapByteBuffer@11fec375[p=0,l=4677,c=8192,r=4677]={<<<<!--\n    This file is her...ct()">\n</BODY>\n\n</HTML>>>>\x00\x00\x00\x00\x00\x00\x00\x00\x00...\x00\x00\x00\x00\x00\x00\x00})
o.e.jetty.server.HttpOutput: write(array) s=CLOSING,api=BLOCKED,sc=false,e=null last=true agg=false flush=true async=false, len=4677 null
org.eclipse.jetty.util.Pool: Pool@63e81ac6[inUse=0,size=1,max=1024,closed=false] returning new reserved entry MonoEntry@c9afe3a{PENDING,pooled=null}
o.e.j.s.h.g.GzipHttpOutputInterceptor: org.eclipse.jetty.server.handler.gzip.GzipHttpOutputInterceptor@3e9a7693 compressing org.eclipse.jetty.util.compression.CompressionPool$Entry@7cc6fdfd
o.e.jetty.server.HttpChannel: sendResponse info=null content=DirectByteBuffer@62ed2d07[p=0,l=1381,c=32768,r=1381]={<<<\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x00\xC5Wmo\xDb6\x10\xFe\xEe_\xC1\xA8hb7...H\xFay\xE8\xE8OIm\xA9\x11\xAe\xF94\xFe\x1bC\x7f\xA1\x06E\x12\x00\x00>>>\x00\x00\x00\x00\x00\x00\x00\x00\x00...\x00\x00\x00\x00\x00\x00\x00} complete=true committing=true callback=GzipBufferCB@2cfe67df[content=HeapByteBuffer@718d0efa[p=4677,l=4677,c=8192,r=0]={<!--\n    ...</HTML><<<>>>\x00\x00\x00\x00\x00\x00\x00\x00\x00...\x00\x00\x00\x00\x00\x00\x00} last=true buffer=DirectByteBuffer@62ed2d07[p=0,l=1381,c=32768,r=1381]={<<<\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x00\xC5Wmo\xDb6\x10\xFe\xEe_\xC1\xA8hb7...H\xFay\xE8\xE8OIm\xA9\x11\xAe\xF94\xFe\x1bC\x7f\xA1\x06E\x12\x00\x00>>>\x00\x00\x00\x00\x00\x00\x00\x00\x00...\x00\x00\x00\x00\x00\x00\x00} deflate=null ]
o.e.jetty.server.HttpChannel: {"message":"COMMIT for /gateway/login.html on HttpChannelOverHttp@6cd392a9{s=HttpChannelState@11eb0f8{s=HANDLING rs=BLOCKING os=COMMITTED is=IDLE awp=false se=false i=true al=1},r=3,c=false/false,a=HANDLING,uri=https://example.com:9310/gateway/login.html?TENANT=rorywin1&ts=1731577382989&redirect=https://example.com:9320/rorywin1/admin/,age=345}\n200 null HTTP/1.1\nDate: Thu, 14 Nov 2024 09:43:09 GMT\r\nCAF-Correlation-Id: d674db41-1124-4bec-b70b-29571f67b0fb\r\nVary: Origin, Accept-Encoding\r\nX-Frame-Options: SAMEORIGIN\r\nContent-Security-Policy: frame-src 'self'; frame-ancestors 'self'; object-src 'none';\r\nX-Content-Type-Options: nosniff\r\nX-Robots-Tag: none\r\nX-XSS-Protection: 1; mode=block\r\nStrict-Transport-Security: max-age=31536000; includeSubDomains\r\nAuthorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIxeDEzUEwtYmFCVjVIY1Q1X0NGU1BVQ0VGRUNycnZfdVhneXJPT1hLWlhzIn0.eyJleHAiOjE3MzE1Nzc2ODgsImlhdCI6MTczMTU3NzM4OCwiYXV0aF90aW1lIjoxNzMxNTc3Mzg4LCJqdGkiOiI1YTYxNGNhYy0yOGIyLTQ5NDktYmNhOS02ODdlMWFjM2Q5NjYiLCJpc3MiOiJodHRwczovL2xhcnJ5LWV4dDAxLnN3aW5mcmEubmV0OjkwMjUvYXV0aC9yZWFsbXMvcm9yeXdpbjEiLCJhdWQiOlsicmVhbG0tbWFuYWdlbWVudCIsImFjY291bnQiXSwic3ViIjoiZmFkNzhjMDktOWQ0Ni00MGNjLTg1MjYtODQ2Mjg2ZDYwMGY0IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiY2EiLCJzaWQiOiJhNTQ0NjRkZC02ZGRmLTQwNTAtYjk3My1iMzk5MjE2N2NkZTEiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVzb3VyY2VfYWNjZXNzIjp7InJlYWxtLW1hbmFnZW1lbnQiOnsicm9sZXMiOlsidmlldy11c2VycyIsInF1ZXJ5LWdyb3VwcyIsInF1ZXJ5LXVzZXJzIl19LCJjYSI6eyJyb2xlcyI6WyJFY2FNYW5hZ2VDdXN0b2RpYW4iLCJFY2FBc3NpZ25Ub1dvcmtib29rIiwiRGF0YU1hbmFnZW1lbnRNYW5hZ2VBZ2VudHMiLCJBbmFseXplUmVzZWFyY2giLCJBc3BlbkFkbWluVmlld1VzYWdlIiwiRWNhQ29sbGVjdERhdGEiLCJFY2FNYW5hZ2VDYXRlZ29yeSIsIkFuYWx5emVWaWV3RW50aXR5VmFsdWVzIiwiQW5hbHl6ZVZpZXdEYXNoYm9hcmRzIiwiRWNhU2VuZFRvVGFyZ2V0IiwiRWNhTWFuYWdlQ2FzZSIsIkVjYUFzc2lnblRvVGFnIiwiRWNhU2VjdXJlIiwiRGF0YU1hbmFnZW1lbnRNYW5hZ2VTZWN1cmVTeXN0ZW1zIiwiRWNhQ29udGVudFNlYXJjaCIsIkVjYVJldmlld0dyYW1tYXJzIiwiRWNhTWFuYWdlRGlzcG9zaXRpb24iLCJFY2FSZWxlYXNlSG9sZCIsIkRhdGFNYW5hZ2VtZW50RGVsZXRlUmVwb3NpdG9yaWVzIiwiQXNwZW5BZG1pbkFwaURldmVsb3BlciIsIkFzcGVuQWRtaW5Vc2VyTWFuYWdlbWVudCIsIkVjYU1hbmFnZUV4cG9ydCIsIkVjYVZpZXdBdWRpdCIsIkRhdGFNYW5hZ2VtZW50TWFuYWdlR3JhbW1hcnMiLCJFY2FNYW5hZ2VIb2xkIiwiRWNhQ29uZmlndXJlIiwiRWNhT2NyIiwiRGF0YU1hbmFnZW1lbnRNYW5hZ2VEZXN0aW5hdGlvbnMiLCJBbmFseXplQXNzaWduVG9UYWciLCJFY2FFeHBvcnREYXRhIiwiRWNhTWFuYWdlV29ya2Jvb2siLCJBbmFseXplUHJldmlldyIsIkVjYUFwcHJvdmVQb2xpY2llcyIsIkFuYWx5emVEb3dubG9hZCIsIkRhdGFNYW5hZ2VtZW50TWFuYWdlVGFncyIsIkFzcGVuQWRtaW5WaWV3QXVkaXQiLCJEYXRhTWFuYWdlbWVudE1hbmFnZVJlcG9zaXRvcmllcyIsIkRhdGFNYW5hZ2VtZW50TWFuYWdlU2VjdXJlUnVsZXMiLCJFY2FBc3NpZ25Ub0hvbGQiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJSb3J5IFRvcm5leSIsInByZWZlcnJlZF91c2VybmFtZSI6InJvcnkudG9ybmV5QG1pY3JvZm9jdXMuY29tIiwiZ2l2ZW5fbmFtZSI6IlJvcnkiLCJmYW1pbHlfbmFtZSI6IlRvcm5leSIsImVtYWlsIjoicm9yeS50b3JuZXlAbWljcm9mb2N1cy5jb20ifQ.Du0J9j1eniMNK66gDey4y5NpJ61hJXI4TR6SsZBwnH0ZopkRGA_MoDvO7eRIolJ8X1_GZRbz9FdWMLG_tVs-GmZ5siSUWjY65holWYJkeYmisX5biwfKj9K1fWjGVOJacSw_gZLETNVPx9FWTfMVd2j7IqvdPibTi_H1AJySGtMn2xNCeIJ5A-0yVDOJJkNiPPWLR77ZcBzJ3IpmIrDwLOT07Te2VRXWWw-E4yf8oh3ru_skYZSXi-xmAqZDYO2jEkJ1GL-B3kcDDlHmixm6rRaWWkhdN5mICK_6R8jkWMjLJ5bKPh_GdN3l4xEn44GDxRsNGbZc0llxsOQHVDnFaA\r\nSet-Cookie: refresh-token=eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJjNTYxZGQ2Zi1lYmY3LTQyZTAtYWU1ZC05ZTZjODA1YjM5ZDkifQ.eyJleHAiOjE3MzE1NzkxODgsImlhdCI6MTczMTU3NzM4OCwianRpIjoiYmI0OTA2MjctMzZmZC00YTRhLTgyZGQtMGFlOGJhMWQ1ZGU1IiwiaXNzIjoiaHR0cHM6Ly9sYXJyeS1leHQwMS5zd2luZnJhLm5ldDo5MDI1L2F1dGgvcmVhbG1zL3Jvcnl3aW4xIiwiYXVkIjoiaHR0cHM6Ly9sYXJyeS1leHQwMS5zd2luZnJhLm5ldDo5MDI1L2F1dGgvcmVhbG1zL3Jvcnl3aW4xIiwic3ViIjoiZmFkNzhjMDktOWQ0Ni00MGNjLTg1MjYtODQ2Mjg2ZDYwMGY0IiwidHlwIjoiUmVmcmVzaCIsImF6cCI6ImNhIiwic2lkIjoiYTU0NDY0ZGQtNmRkZi00MDUwLWI5NzMtYjM5OTIxNjdjZGUxIiwic2NvcGUiOiJvcGVuaWQgYmFzaWMgcHJvZmlsZSBhY3IgZW1haWwgcm9sZXMgd2ViLW9yaWdpbnMifQ.3PgtfZhv_v9s7r60OR5PufrKq1GrVXwnmVuvRnDwZqg30yAnPePriZekLLGQEeOkZOmEWk7nW18nwKEE8klIUA; Path=/v1/auth/cookie/; Secure; HttpOnly\r\nExpires: Thu, 01 Jan 1970 00:00:00 GMT\r\nSet-Cookie: csrf-token=73e3671db0c34386bd097df1ba82eee01015744945297452179; Path=/; Secure\r\nContent-Type: text/html\r\nContent-Encoding: gzip\r\n\r\n"}
o.eclipse.jetty.server.Request: Response Request(GET https://example.com:9310/gateway/login.html?TENANT=rorywin1&ts=1731577382989&redirect=https://example.com:9320/rorywin1/admin/)@778c1821 committing for session Session@32bca397{id=node0v7yrxsrt309014zqxnoips8tm1,x=node0v7yrxsrt309014zqxnoips8tm1.node0,req=1,res=true}
o.e.j.server.HttpConnection: generate: NEED_HEADER for SendCallback@34bcc3d7[PROCESSING][i=HTTP/1.1{s=200,h=15,cl=-1},cb=org.eclipse.jetty.server.HttpChannel$SendCallback@58309332] (null,[p=0,l=1381,c=32768,r=1381],true)@START
o.e.jetty.http.HttpGenerator: generateHeaders HTTP/1.1{s=200,h=15,cl=-1} last=true content=DirectByteBuffer@62ed2d07[p=0,l=1381,c=32768,r=1381]={<<<\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x00\xC5Wmo\xDb6\x10\xFe\xEe_\xC1\xA8hb7...H\xFay\xE8\xE8OIm\xA9\x11\xAe\xF94\xFe\x1bC\x7f\xA1\x06E\x12\x00\x00>>>\x00\x00\x00\x00\x00\x00\x00\x00\x00...\x00\x00\x00\x00\x00\x00\x00}
o.e.jetty.http.HttpGenerator: {"message":"Date: Thu, 14 Nov 2024 09:43:09 GMT\r\nCAF-Correlation-Id: d674db41-1124-4bec-b70b-29571f67b0fb\r\nVary: Origin, Accept-Encoding\r\nX-Frame-Options: SAMEORIGIN\r\nContent-Security-Policy: frame-src 'self'; frame-ancestors 'self'; object-src 'none';\r\nX-Content-Type-Options: nosniff\r\nX-Robots-Tag: none\r\nX-XSS-Protection: 1; mode=block\r\nStrict-Transport-Security: max-age=31536000; includeSubDomains\r\nAuthorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIxeDEzUEwtYmFCVjVIY1Q1X0NGU1BVQ0VGRUNycnZfdVhneXJPT1hLWlhzIn0.eyJleHAiOjE3MzE1Nzc2ODgsImlhdCI6MTczMTU3NzM4OCwiYXV0aF90aW1lIjoxNzMxNTc3Mzg4LCJqdGkiOiI1YTYxNGNhYy0yOGIyLTQ5NDktYmNhOS02ODdlMWFjM2Q5NjYiLCJpc3MiOiJodHRwczovL2xhcnJ5LWV4dDAxLnN3aW5mcmEubmV0OjkwMjUvYXV0aC9yZWFsbXMvcm9yeXdpbjEiLCJhdWQiOlsicmVhbG0tbWFuYWdlbWVudCIsImFjY291bnQiXSwic3ViIjoiZmFkNzhjMDktOWQ0Ni00MGNjLTg1MjYtODQ2Mjg2ZDYwMGY0IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiY2EiLCJzaWQiOiJhNTQ0NjRkZC02ZGRmLTQwNTAtYjk3My1iMzk5MjE2N2NkZTEiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVzb3VyY2VfYWNjZXNzIjp7InJlYWxtLW1hbmFnZW1lbnQiOnsicm9sZXMiOlsidmlldy11c2VycyIsInF1ZXJ5LWdyb3VwcyIsInF1ZXJ5LXVzZXJzIl19LCJjYSI6eyJyb2xlcyI6WyJFY2FNYW5hZ2VDdXN0b2RpYW4iLCJFY2FBc3NpZ25Ub1dvcmtib29rIiwiRGF0YU1hbmFnZW1lbnRNYW5hZ2VBZ2VudHMiLCJBbmFseXplUmVzZWFyY2giLCJBc3BlbkFkbWluVmlld1VzYWdlIiwiRWNhQ29sbGVjdERhdGEiLCJFY2FNYW5hZ2VDYXRlZ29yeSIsIkFuYWx5emVWaWV3RW50aXR5VmFsdWVzIiwiQW5hbHl6ZVZpZXdEYXNoYm9hcmRzIiwiRWNhU2VuZFRvVGFyZ2V0IiwiRWNhTWFuYWdlQ2FzZSIsIkVjYUFzc2lnblRvVGFnIiwiRWNhU2VjdXJlIiwiRGF0YU1hbmFnZW1lbnRNYW5hZ2VTZWN1cmVTeXN0ZW1zIiwiRWNhQ29udGVudFNlYXJjaCIsIkVjYVJldmlld0dyYW1tYXJzIiwiRWNhTWFuYWdlRGlzcG9zaXRpb24iLCJFY2FSZWxlYXNlSG9sZCIsIkRhdGFNYW5hZ2VtZW50RGVsZXRlUmVwb3NpdG9yaWVzIiwiQXNwZW5BZG1pbkFwaURldmVsb3BlciIsIkFzcGVuQWRtaW5Vc2VyTWFuYWdlbWVudCIsIkVjYU1hbmFnZUV4cG9ydCIsIkVjYVZpZXdBdWRpdCIsIkRhdGFNYW5hZ2VtZW50TWFuYWdlR3JhbW1hcnMiLCJFY2FNYW5hZ2VIb2xkIiwiRWNhQ29uZmlndXJlIiwiRWNhT2NyIiwiRGF0YU1hbmFnZW1lbnRNYW5hZ2VEZXN0aW5hdGlvbnMiLCJBbmFseXplQXNzaWduVG9UYWciLCJFY2FFeHBvcnREYXRhIiwiRWNhTWFuYWdlV29ya2Jvb2siLCJBbmFseXplUHJldmlldyIsIkVjYUFwcHJvdmVQb2xpY2llcyIsIkFuYWx5emVEb3dubG9hZCIsIkRhdGFNYW5hZ2VtZW50TWFuYWdlVGFncyIsIkFzcGVuQWRtaW5WaWV3QXVkaXQiLCJEYXRhTWFuYWdlbWVudE1hbmFnZVJlcG9zaXRvcmllcyIsIkRhdGFNYW5hZ2VtZW50TWFuYWdlU2VjdXJlUnVsZXMiLCJFY2FBc3NpZ25Ub0hvbGQiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsIm5hbWUiOiJSb3J5IFRvcm5leSIsInByZWZlcnJlZF91c2VybmFtZSI6InJvcnkudG9ybmV5QG1pY3JvZm9jdXMuY29tIiwiZ2l2ZW5fbmFtZSI6IlJvcnkiLCJmYW1pbHlfbmFtZSI6IlRvcm5leSIsImVtYWlsIjoicm9yeS50b3JuZXlAbWljcm9mb2N1cy5jb20ifQ.Du0J9j1eniMNK66gDey4y5NpJ61hJXI4TR6SsZBwnH0ZopkRGA_MoDvO7eRIolJ8X1_GZRbz9FdWMLG_tVs-GmZ5siSUWjY65holWYJkeYmisX5biwfKj9K1fWjGVOJacSw_gZLETNVPx9FWTfMVd2j7IqvdPibTi_H1AJySGtMn2xNCeIJ5A-0yVDOJJkNiPPWLR77ZcBzJ3IpmIrDwLOT07Te2VRXWWw-E4yf8oh3ru_skYZSXi-xmAqZDYO2jEkJ1GL-B3kcDDlHmixm6rRaWWkhdN5mICK_6R8jkWMjLJ5bKPh_GdN3l4xEn44GDxRsNGbZc0llxsOQHVDnFaA\r\nSet-Cookie: refresh-token=eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJjNTYxZGQ2Zi1lYmY3LTQyZTAtYWU1ZC05ZTZjODA1YjM5ZDkifQ.eyJleHAiOjE3MzE1NzkxODgsImlhdCI6MTczMTU3NzM4OCwianRpIjoiYmI0OTA2MjctMzZmZC00YTRhLTgyZGQtMGFlOGJhMWQ1ZGU1IiwiaXNzIjoiaHR0cHM6Ly9sYXJyeS1leHQwMS5zd2luZnJhLm5ldDo5MDI1L2F1dGgvcmVhbG1zL3Jvcnl3aW4xIiwiYXVkIjoiaHR0cHM6Ly9sYXJyeS1leHQwMS5zd2luZnJhLm5ldDo5MDI1L2F1dGgvcmVhbG1zL3Jvcnl3aW4xIiwic3ViIjoiZmFkNzhjMDktOWQ0Ni00MGNjLTg1MjYtODQ2Mjg2ZDYwMGY0IiwidHlwIjoiUmVmcmVzaCIsImF6cCI6ImNhIiwic2lkIjoiYTU0NDY0ZGQtNmRkZi00MDUwLWI5NzMtYjM5OTIxNjdjZGUxIiwic2NvcGUiOiJvcGVuaWQgYmFzaWMgcHJvZmlsZSBhY3IgZW1haWwgcm9sZXMgd2ViLW9yaWdpbnMifQ.3PgtfZhv_v9s7r60OR5PufrKq1GrVXwnmVuvRnDwZqg30yAnPePriZekLLGQEeOkZOmEWk7nW18nwKEE8klIUA; Path=/v1/auth/cookie/; Secure; HttpOnly\r\nExpires: Thu, 01 Jan 1970 00:00:00 GMT\r\nSet-Cookie: csrf-token=73e3671db0c34386bd097df1ba82eee01015744945297452179; Path=/; Secure\r\nContent-Type: text/html\r\nContent-Encoding: gzip\r\n\r\n"}
o.e.jetty.http.HttpGenerator: CONTENT_LENGTH
o.e.j.server.HttpConnection: generate: FLUSH for SendCallback@34bcc3d7[PROCESSING][i=HTTP/1.1{s=200,h=15,cl=-1},cb=org.eclipse.jetty.server.HttpChannel$SendCallback@58309332] ([p=0,l=4004,c=8192,r=4004],[p=0,l=1381,c=32768,r=1381],true)@COMPLETING
o.e.jetty.io.WriteFlusher: write: WriteFlusher@60fae849{IDLE}->null [DirectByteBuffer@508aef64[p=0,l=4004,c=8192,r=4004]={<<<HTTP/1.1 200 OK\r\nDate: Th...ontent-Length: 1381\r\n\r\n>>>\x00\x00\x00\x00\x00\x00\x00\x00\x00...\x00\x00\x00\x00\x00\x00\x00},DirectByteBuffer@62ed2d07[p=0,l=1381,c=32768,r=1381]={<<<\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x00\xC5Wmo\xDb6\x10\xFe\xEe_\xC1\xA8hb7...H\xFay\xE8\xE8OIm\xA9\x11\xAe\xF94\xFe\x1bC\x7f\xA1\x06E\x12\x00\x00>>>\x00\x00\x00\x00\x00\x00\x00\x00\x00...\x00\x00\x00\x00\x00\x00\x00}]
o.e.jetty.io.WriteFlusher: update WriteFlusher@60fae849{WRITING}->null:IDLE-->WRITING
o.e.jetty.io.ssl.SslConnection: >flush SslConnection@3e30b117::SocketChannelEndPoint@3c5a3f54[{l=/192.168.75.220:8082,r=/192.168.75.1:53597,OPEN,fill=-,flush=-,to=354/30000}{io=0/0,kio=0,kro=1}]->[SslConnection@3e30b117{NOT_HANDSHAKING,eio=-1/-1,di=-1,fill=IDLE,flush=IDLE}~>{l=/192.168.75.220:8082,r=/192.168.75.1:53597,OPEN,fill=-,flush=W,to=546/30000}=>HttpConnection@5d2af0a8[p=HttpParser{s=CONTENT,0 of -1},g=HttpGenerator@4add5f59{s=COMPLETING}]=>HttpChannelOverHttp@6cd392a9{s=HttpChannelState@11eb0f8{s=HANDLING rs=BLOCKING os=COMMITTED is=IDLE awp=false se=false i=true al=1},r=3,c=false/false,a=HANDLING,uri=https://example.com:9310/gateway/login.html?TENANT=rorywin1&ts=1731577382989&redirect=https://example.com:9320/rorywin1/admin/,age=348}]
o.e.jetty.io.ssl.SslConnection: flush b[0]=DirectByteBuffer@508aef64[p=0,l=4004,c=8192,r=4004]={<<<HTTP/1.1 200 OK\r\nDate: Th...ontent-Length: 1381\r\n\r\n>>>\x00\x00\x00\x00\x00\x00\x00\x00\x00...\x00\x00\x00\x00\x00\x00\x00}

Jetty 12 logs (fail written 4677 > 0 content-length):

c.m.a.g.SecureHeaderFilter: response headers: [Date: Fri, 15 Nov 2024 12:30:41 GMT, Vary: Accept-Encoding, Content-Length: 0]

o.e.j.ee10.servlet.HttpOutput: write(array HeapByteBuffer@4059e9ad[p=0,l=4677,c=8192,r=4677]={<<<<!--\n    This file is her...ct()">\n</BODY>\n\n</HTML>>>>\x00\x00\x00\x00\x00\x00\x00\x00\x00...\x00\x00\x00\x00\x00\x00\x00})
o.e.j.ee10.servlet.HttpOutput: write(array) s=OPEN,api=BLOCKING,sc=false,e=null aggregated !flush ReservedBuffer@9a46aed[rc=1,DirectByteBuffer@50df4a92[p=0,l=4677,c=32768,r=4677]={<<<<!--\n    This file is her...ct()">\n</BODY>\n\n</HTML>>>>\x00\x00\x00\x00\x00\x00\x00\x00\x00...\x00\x00\x00\x00\x00\x00\x00}]
o.e.j.e.s.ServletChannelState: unhandle ServletChannelState@13223dfa{s=HANDLING rs=BLOCKING os=OPEN is=IDLE awp=false se=false i=true al=0}
o.e.j.e.s.ServletChannelState: nextAction(false) COMPLETE ServletChannelState@13223dfa{s=HANDLING rs=COMPLETING os=OPEN is=IDLE awp=false se=false i=false al=0}
o.e.j.e.servlet.ServletChannel: action COMPLETE ServletChannel@35ff7cd1{s=ServletChannelState@13223dfa{s=HANDLING rs=COMPLETING os=OPEN is=IDLE awp=false se=false i=false al=0},r=0,c=false/false,a=HANDLING,uri=https://example.com:9310/gateway/login.html?TENANT=rorywin1&ts=1731666841005&redirect=https://example.com:9320/rorywin1/admin/,age=194}
o.e.j.ee10.servlet.HttpOutput: complete(Callback@32b98b0b{NON_BLOCKING, org.eclipse.jetty.ee10.servlet.ServletChannel$$Lambda/0x00007fac6f69ada0@3ebbfb77,org.eclipse.jetty.ee10.servlet.ServletChannel$$Lambda/0x00007fac6f69afb8@3bbbe09c}) s=CLOSING,api=BLOCKED,sc=false,e=null s=false e=null, c=DirectByteBuffer@50df4a92[p=0,l=4677,c=32768,r=4677]={<<<<!--\n    This file is her...ct()">\n</BODY>\n\n</HTML>>>>\x00\x00\x00\x00\x00\x00\x00\x00\x00...\x00\x00\x00\x00\x00\x00\x00}
o.e.j.s.i.HttpChannelState: fail org.eclipse.jetty.server.handler.ContextResponse$1@7fb14948 written 4677 > 0 content-length
o.e.j.u.t.SerializedInvoker: Offering link Link@602f4767{Queued by dw-35 at org.eclipse.jetty.server.internal.HttpChannelState$ChannelResponse.write(HttpChannelState.java:1290) -> null} of HttpChannelSerializedInvoker@74b44d79{name=HttpChannelState_writeInvoker,tail=null,invoker=null}
o.e.j.u.t.SerializedInvoker: Running link Link@602f4767{Queued by dw-35 at org.eclipse.jetty.server.internal.HttpChannelState$ChannelResponse.write(HttpChannelState.java:1290) -> null} of HttpChannelSerializedInvoker@74b44d79{name=HttpChannelState_writeInvoker,tail=Link@602f4767{Queued by dw-35 at org.eclipse.jetty.server.internal.HttpChannelState$ChannelResponse.write(HttpChannelState.java:1290) -> null},invoker=null}
o.e.j.ee10.servlet.HttpOutput: {"message":"onWriteComplete(true,java.io.IOException: written 4677 > 0 content-length) s=CLOSING,api=BLOCKED,sc=false,e=null->s=CLOSED,api=BLOCKING,sc=false,e=null c=null cb=Callback@32b98b0b{NON_BLOCKING, org.eclipse.jetty.ee10.servlet.ServletChannel$$Lambda/0x00007fac6f69ada0@3ebbfb77,org.eclipse.jetty.ee10.servlet.ServletChannel$$Lambda/0x00007fac6f69afb8@3bbbe09c} w=false","exception":"java.io.IOException: written 4677 > 0 content-length\n\tat org.eclipse.jetty.server.internal.HttpChannelState$ChannelResponse.write(HttpChannelState.java:1271)\n\tat org.eclipse.jetty.server.Response$Wrapper.write(Response.java:768)\n\tat org.eclipse.jetty.server.handler.gzip.GzipResponseAndCallback.write(GzipResponseAndCallback.java:133)\n\tat org.eclipse.jetty.server.Response$Wrapper.write(Response.java:768)\n\tat org.eclipse.jetty.server.handler.ContextResponse.write(ContextResponse.java:56)\n\tat org.eclipse.jetty.ee10.servlet.ServletContextResponse.write(ServletContextResponse.java:288)\n\tat org.eclipse.jetty.ee10.servlet.HttpOutput.channelWrite(HttpOutput.java:206)\n\tat org.eclipse.jetty.ee10.servlet.HttpOutput.complete(HttpOutput.java:437)\n\tat org.eclipse.jetty.ee10.servlet.ServletContextResponse.completeOutput(ServletContextResponse.java:212)\n\tat org.eclipse.jetty.ee10.servlet.ServletChannel.handle(ServletChannel.java:576)\n\tat org.eclipse.jetty.ee10.servlet.ServletHandler.handle(ServletHandler.java:464)\n\tat org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:575)\n\tat org.eclipse.jetty.ee10.servlet.SessionHandler.handle(SessionHandler.java:717)\n\tat org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:1060)\n\tat org.eclipse.jetty.server.Handler$Wrapper.handle(Handler.java:740)\n\tat io.dropwizard.metrics.jetty12.AbstractInstrumentedHandler.handle(AbstractInstrumentedHandler.java:299)\n\tat io.dropwizard.jetty.RoutingHandler.handle(RoutingHandler.java:41)\n\tat org.eclipse.jetty.server.handler.gzip.GzipHandler.handle(GzipHandler.java:597)\n\tat io.dropwizard.jetty.ZipExceptionHandlingGzipHandler.handle(ZipExceptionHandlingGzipHandler.java:21)\n\tat org.eclipse.jetty.server.Handler$Wrapper.handle(Handler.java:740)\n\tat org.eclipse.jetty.server.handler.GracefulHandler.handle(GracefulHandler.java:101)\n\tat org.eclipse.jetty.server.Server.handle(Server.java:182)\n\tat org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:662)\n\tat org.eclipse.jetty.server.internal.HttpConnection.onFillable(HttpConnection.java:414)\n\tat org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:322)\n\tat org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99)\n\tat org.eclipse.jetty.io.ssl.SslConnection$SslEndPoint.onFillable(SslConnection.java:575)\n\tat org.eclipse.jetty.io.ssl.SslConnection.onFillable(SslConnection.java:390)\n\tat org.eclipse.jetty.io.ssl.SslConnection$2.succeeded(SslConnection.java:150)\n\tat org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99)\n\tat org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)\n\tat org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:979)\n\tat org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1209)\n\tat org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1164)\n\tat java.base/java.lang.Thread.run(Thread.java:1583)\n"}

Some points:

Hare are the relevant classes in my app:

ApiGatewayApplication.java

class ApiGatewayApplication{
    @Override
    public void run(final ApiGatewayConfiguration apiGatewayConfiguration, final Environment environment) {

        final String corsOrigins = configuration.getAuthConfiguration().getCorsOrigins();
        if (configuration.getAuthConfiguration().isCors() && !Strings.isNullOrEmpty(corsOrigins)) {
            logger.debug("Adding CORS...");
            final FilterRegistration.Dynamic filter = environment.servlets().addFilter("CORS", CrossOriginFilter.class);
            filter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), true, "/*");
            filter.setInitParameter(CrossOriginFilter.ALLOWED_METHODS_PARAM, "GET,POST,PUT,PATCH,DELETE,OPTIONS");
            filter.setInitParameter(CrossOriginFilter.ALLOWED_HEADERS_PARAM, "Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin,If-Modified-Since,If-Unmodified-Since,Cache-Control,Tenant,Csrf-Token,X-TENANT-ID,CA-AGENT-SERVICE,CAF-Correlation-Id");
            filter.setInitParameter(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, "*");
            filter.setInitParameter(CrossOriginFilter.ALLOW_CREDENTIALS_PARAM, "true");
            filter.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, corsOrigins);
        }

        environment.jersey().register(MultiPartFeature.class);

        final Client httpClient = new JerseyClientBuilder(environment).using(apiGatewayConfiguration.getHttpClientConfiguration()).build(getName());
        httpClient.register(MultiPartWriter.class);
        httpClient.register(FormDataMultiPart.class);
        httpClient.register(new CorrelationIdClientFilter());

        final LoginResource loginResource = new LoginResource(apiGatewayConfiguration);
        environment.jersey().register(loginResource);

        //this adds secure headers to all responses
        environment.servlets().addFilter("SecureHeaderFilter", new SecureHeaderFilter()).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ERROR), true, "/*");

        //this adds the access token to the response header after login, so that the token can be sent back in subsequent rest calls
        environment.servlets().addFilter("CustomServletFilter", new CustomServletFilter(apiGatewayConfiguration.getAuthConfiguration().isSecureSession())).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, ApiGatewayConstants.SWAGGER_URL + "/", ApiGatewayConstants.SWAGGER_URL + ".json", "/gateway/login.html");
        //if need to intercept response, can add class (which implements ContainerResponseFilter) to env.jersey.register()
    }
}

CustomServletFilter.java

import java.io.IOException;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.ws.rs.WebApplicationException;

import org.keycloak.adapters.RefreshableKeycloakSecurityContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

//this adds the access token to the response header after login, so that the token can be sent back in subsequent rest calls
public class CustomServletFilter implements jakarta.servlet.Filter {
    private static final Logger logger = LoggerFactory.getLogger(CustomServletFilter.class);

    private boolean isSecureSession = true;

    public CustomServletFilter() {
    }

    public CustomServletFilter(final boolean isSecureSession) {
        this.isSecureSession = isSecureSession;
    }

    @Override
    public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
        if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
            final HttpServletRequest httpRequest = (HttpServletRequest) request;
            final HttpServletResponse httpResponse = (HttpServletResponse) response;

            final RefreshableKeycloakSecurityContext keycloakContext = ApiGatewayHelper.getKeycloakContext(httpRequest);
            if (keycloakContext != null) {
                httpResponse.setHeader(IDMHelper.HEADER_AUTH, IDMHelper.HEADER_AUTH_BEARER + keycloakContext.getTokenString());

                final Cookie refreshTokenCookie = new Cookie(ApiGatewayConstants.REFRESH_TOKEN_KEY, keycloakContext.getRefreshToken());
                refreshTokenCookie.setPath(ApiGatewayConstants.REFRESH_TOKEN_COOKIE_PATH); //only needs to be sent back to cookie methods
                refreshTokenCookie.setHttpOnly(true); //should not be read by client
                refreshTokenCookie.setSecure(isSecureSession);
                refreshTokenCookie.setMaxAge(-1);
                httpResponse.addCookie(refreshTokenCookie);
                logger.debug("Logged in and added refresh token to cookie");
                logger.trace("New Refresh Token: {}", keycloakContext.getRefreshToken());

                if (!httpRequest.getRequestURI().contains(ApiGatewayConstants.SWAGGER_URL)) {
                    final String csrfUuid = UUID.randomUUID().toString().replace("-", "");
                    final BigInteger csrfRandomNum = new BigInteger(64, new SecureRandom());

                    final Cookie csrfTokenCookie = new Cookie(ApiGatewayConstants.CSRF_TOKEN_KEY, csrfUuid + csrfRandomNum);
                    csrfTokenCookie.setPath("/"); //needs to be sent back on all calls
                    csrfTokenCookie.setHttpOnly(false); //needs to be read by client
                    csrfTokenCookie.setSecure(isSecureSession);
                    csrfTokenCookie.setMaxAge(-1);
                    httpResponse.addCookie(csrfTokenCookie);
                }
                else {
                    //check if user has swagger permission
                    final List<String> perms = new ArrayList<>(Arrays.asList(ApplicationPermissions.AdminPermission.AspenAdminApiDeveloper.name(), ApiGatewayConstants.KEYCLOAK_ONBOARD_PERMISSION, ApiGatewayConstants.KEYCLOAK_USAGE_PERMISSION));
                    if (!Authorizer.hasRoles(keycloakContext.getToken(), perms, true)) {
                        logger.error("No API Developer Permission");
                        throw new WebApplicationException("Forbidden");
                    }

                    httpResponse.setHeader("Cache-Control", "no-store");
                }
            }
        }

        chain.doFilter(request, response);
    }

    @Override
    public void init(final FilterConfig filterConfig) {
    }

    @Override
    public void destroy() {
    }
}

SecureHeaderFilter.java

import java.io.IOException;

import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

//this adds the access token to the response header after login, so that the token can be sent back in subsequent rest calls
public class SecureHeaderFilter implements jakarta.servlet.Filter {
    private static final Logger logger = LoggerFactory.getLogger(SecureHeaderFilter.class);

    public SecureHeaderFilter() {
    }

    @Override
    public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
        if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
            final HttpServletRequest httpRequest = (HttpServletRequest) request;
            final HttpServletResponse httpResponse = (HttpServletResponse) response;

            if (!httpRequest.getMethod().equalsIgnoreCase("OPTIONS")) {
                //these are based on the keycloak secure headers
                httpResponse.setHeader("X-Frame-Options", "SAMEORIGIN");
                httpResponse.setHeader("Content-Security-Policy", "frame-src 'self'; frame-ancestors 'self'; object-src 'none';");
                httpResponse.setHeader("X-Content-Type-Options", "nosniff");
                httpResponse.setHeader("X-Robots-Tag", "none");
                httpResponse.setHeader("X-XSS-Protection", "1; mode=block");
                httpResponse.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
            }

            logger.warn("response headers: " + getHeaders(httpResponse));
        }

        chain.doFilter(request, response);
    }

    @Override
    public void init(final FilterConfig filterConfig) {
    }

    @Override
    public void destroy() {
    }

    private static List<String> getHeaders(HttpServletResponse response) {

        List<String> headerLogs = new ArrayList<>();

        try {
            Collection<String> headerNames = response.getHeaderNames();
            if (headerNames == null) {
                return Collections.emptyList();
            }

            for (String headerName: headerNames) {
                Collection<String> headerValues = response.getHeaders(headerName);
                if (headerValues == null) {
                    headerLogs.add(String.format("%s: <null>", headerName));
                    continue;
                }

                StringBuilder valueBuilder = new StringBuilder();
                for (String headerValue : headerValues) {
                    if (!valueBuilder.isEmpty()) {
                        valueBuilder.append(", ");
                    }
                    valueBuilder.append(headerValue != null ? headerValue : "<null>");
                }

                headerLogs.add(String.format("%s: %s",
                        headerName,
                        !valueBuilder.isEmpty() ? valueBuilder.toString() : "<empty>"));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return headerLogs;
    }
}

LoginResource.java

import io.swagger.v3.oas.annotations.Operation;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/gateway/login.html")
public class LoginResource {
    private static final Logger logger = LoggerFactory.getLogger(LoginResource.class);

    private final ApiGatewayConfiguration apiGatewayConfiguration;

    public LoginResource(final ApiGatewayConfiguration apiGatewayConfiguration) {
        this.apiGatewayConfiguration = apiGatewayConfiguration;

        logger.warn("LoginResource invoked");
    }

    @Operation(hidden = true)
    @GET
    @Produces(MediaType.TEXT_HTML)
    public LoginView Login() {
        logger.warn("Login resource called returning LoginView instance");

        return new LoginView(apiGatewayConfiguration.getAuthConfiguration());
    }
}

LoginView.java

import io.dropwizard.views.common.View;

public class LoginView extends View {
    private final AuthConfiguration loginConfig;

    public LoginView(AuthConfiguration loginConfig) {
        super("login.ftl");
        this.loginConfig = loginConfig;
    }

    public AuthConfiguration getLoginConfig() {
        return loginConfig;
    }
}

login.ftl

<HTML>
<HEAD>
    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
    <meta http-equiv="Pragma" content="no-cache" />
    <meta http-equiv="Expires" content="0" />
</HEAD>

<SCRIPT>
    function redirect() {
        var LOCALHOST = "localhost";
        var REDIRECT_KEY = "redirect";
        var REDIRECT_ERROR_KEY = "redirectError";
        var ERROR_KEY = "error";

        var redirectWhitelistString = "${loginConfig.redirectWhitelist}";
        var redirectWhitelist = redirectWhitelistString.split(",");

        var redirect = "";
        var redirectError = "";
        var error = "";
        var serverName, redirectServerName;
        var location = window.location.toString();
        serverName = getServerName(location);
        var startOfParamsPosition = location.indexOf("?");
        if (startOfParamsPosition > -1) {
            var params = location.substring(startOfParamsPosition + 1).split("&");
            for (var i = 0; i < params.length; i++) {
                var param = params[i];
                var splitParam = param.split("=");
                if (splitParam.length > 1) {
                    if (splitParam[0] === REDIRECT_KEY) {
                        redirect = splitParam[1].replace(/%25/g, "%").replace("%23", "#").replace("%3D", "=");
                    }
                    else if (splitParam[0] === REDIRECT_ERROR_KEY) {
                        redirectError = splitParam[1].replace("%25", "%").replace("%23", "#");
                    }
                    else if (splitParam[0] === ERROR_KEY) {
                        error = splitParam[1];
                    }
                }
            }
        }
        if (error.length > 0 && redirectError.length > 0) {
            redirectServerName = getServerName(redirectError);
            if (isValidServer(serverName, redirectServerName) > -1) {
                window.location = redirectError;
            }
            else {
                document.getElementById("pageBody").innerHTML = "Invalid Redirect Server";
            }
        }
        else if (redirect.length > 0) {
            redirectServerName = getServerName(redirect);
            var validServer = isValidServer(serverName, redirectServerName);
            if (validServer === 0) {
                window.location = redirect;
            }
            else if (validServer === 1) {
                //ui and gateway servers are not same so need to pass token in url
                if (document.cookie.indexOf("csrf-token") !== -1) {
                    redirect += "&" + document.cookie;
                }
                window.location = redirect;
            }
            else {
                document.getElementById("pageBody").innerHTML = "Invalid Redirect Server";
            }
        }
        else {
            document.getElementById("pageBody").innerHTML = "No Redirect Specified!";
        }

        function isValidServer(serverName, redirectServerName) {
            if (redirectServerName === serverName) {
                return 0;
            }
            else if (redirectServerName === LOCALHOST || serverName === LOCALHOST || redirectWhitelist.indexOf('*') >= 0 || redirectWhitelist.indexOf(redirectServerName) >= 0) {
                return 1;
            }
            else {
                return -1;
            }
        }

        function getServerName (url) {
            var startOfServerNamePosition = url.indexOf("://");
            if (startOfServerNamePosition > -1) {
                var endOfServerNamePosition = url.indexOf(":", startOfServerNamePosition + 1);
                if (endOfServerNamePosition > -1) {
                    return url.substring(startOfServerNamePosition + 3, endOfServerNamePosition);
                }
            }
            return "";
        }
    }
</SCRIPT>

<BODY id="pageBody" onload="redirect()">
</BODY>

</HTML>
joakime commented 1 week ago

Duplicate of #12481

Fixed in PR #12484

Will be available in release 12.0.16 due out end of November.

rorytorneymf commented 1 week ago

Thanks, I think that is a different issue to the one I have though, I temporarily reverted to 12.0.2 (released Oct 10, 2023), which is before the change (Dec 6, 2023) that introduced the problem was added, and still get the same error. It is possibly something I'm doing wrong in my app, I'll continue investigating.

joakime commented 1 week ago

I'll reopen.

If you can make your scattered source files into a ready to run project that would be great (just a new project on github should be sufficient). Use maven or gradle, and some simple instructions on how/what to run to trigger the issue. (also if the issue occurs every time, or occasionally)

From that, we can confirm if this is a duplicate of #12481

rorytorneymf commented 1 week ago

Apologies, closing this. This was happening because I was calling callback.succeeded() which was causing the response to be committed prematurely.

joakime commented 1 week ago

@rorytorneymf where are you doing that? The code you presented in your question has no jetty core Handler example present.

rorytorneymf commented 1 week ago

It was in a separate class I didn't think was the issue, this is the particular method:

@Override
public AuthenticationState validateRequest(Request req, Response res, Callback callback) throws ServerAuthException {
    final ServletContextRequest servletContextRequest = (ServletContextRequest)req;
    Request request = resolveRequest(servletContextRequest.getServletApiRequest());

    final ServletContextResponse servletContextResponse = (ServletContextResponse)res;
    OIDCJettyHttpFacade facade = new OIDCJettyHttpFacade(request, servletContextResponse.getServletApiResponse().getResponse());
    KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
    if (deployment == null || !deployment.isConfigured()) {
        callback.succeeded();
        return AuthenticationState.SEND_FAILURE; // TODO how to send .UNAUTHENTICATED?
    }
    PreAuthActionsHandler handler = new PreAuthActionsHandler(createSessionManagement(request), deploymentContext, facade);
    if (handler.handleRequest()) {
        callback.succeeded();
        return AuthenticationState.SEND_SUCCESS;
    }

    AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment);
    nodesRegistrationManagement.tryRegister(deployment);

    tokenStore.checkCurrentToken();
    JettyRequestAuthenticator authenticator = createRequestAuthenticator(request, facade, deployment, tokenStore);
    AuthOutcome outcome = authenticator.authenticate();
    if (outcome == AuthOutcome.AUTHENTICATED) {
        if (facade.isEnded()) {
            callback.succeeded();
            return AuthenticationState.SEND_SUCCESS;
        }

        AuthenticationState authenticationState = register(request, authenticator.principal);
        AuthenticatedActionsHandler authenticatedActionsHandler = new AuthenticatedActionsHandler(deployment, facade);
        if (authenticatedActionsHandler.handledRequest()) {
            callback.succeeded();
            return AuthenticationState.SEND_SUCCESS;
        }
        final KeycloakAuthentication ka = (KeycloakAuthentication)authenticationState;
        // Important: do no call callback.succeeded() here as it results in the response being committed too early.
        // E.g. the login.html is returned with a Content-Length: 0 header even though there are >0 bytes in the content.
        return authenticationState;
    }
    AuthChallenge challenge = authenticator.getChallenge();
    if (challenge != null) {
        challenge.challenge(facade);
    }
    callback.succeeded();
    return AuthenticationState.CHALLENGE;
}