pedestal 2026-02-23

Hiya! I'm continuing to look at stylobate errors. I could very well be missing something, but I seem to get a 200 response with an empty body on stylobate errors. 🧵

There's a bit about having to discern the intentions of the Original Gangsters in some of this lowest-level code. I think the intention is to not let exceptions get this far because we are long past the point where we can do much about it (such as send a reasonable error response).

Thanks @hlship, I think that is reasonable here.

Here's my little playground:

(ns pedestal-stylobate-playground
  (:require [babashka.http-client :as http]
            [io.pedestal.connector :as conn]
            [io.pedestal.http.impl.servlet-interceptor :as psi]
            [io.pedestal.http.jetty :as jetty]))

(defn- start-server [{:keys [port]}]
  (let [connector (-> (conn/default-connector-map port)
                      (conn/with-default-interceptors)
                      (conn/with-routes #{["/greet"
                                           :get (fn greet-handler [_request]
                                                  {:status 200
                                                   :body   "Hello, world!\n"})
                                           :route-name :greet]})
                      (jetty/create-connector {:join? false}))]
    (conn/start! connector)))

(comment
  (def server (start-server {:port 8133}))

  (select-keys  (http/get "") [:status :body])
  ;; => {:status 200, :body "Hello, world!\n"}

  ;; simulate a problem writing response
  (with-redefs [psi/write-body (fn [_servlet-resp body]
                                 (throw (ex-info "Blarpo" {})))]
    (select-keys (http/get "") [:status :body]))
  ;; => {:status 200, :body ""}

  (conn/stop! server)

  :eoc)
For my "simulate a problem" call, I do see the stylobate error logged:
ERROR [2026-02-23 10:30:08,985] qtp222342859-208 - io.pedestal.http.impl.servlet-interceptor {:msg "error-stylobate triggered", :context {:response {:status 200, :body "Hello, world!\n", :headers {"Strict-Transport-Security" "max-age=31536000; includeSubdomains", "X-Frame-Options" "DENY", "X-Content-Type-Options" "nosniff", "X-XSS-Protection" "1; mode=block", "X-Download-Options" "noopen", "X-Permitted-Cross-Domain-Policies" "none", "Content-Security-Policy" "object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;", "Content-Type" "text/plain"}}, :io.pedestal.interceptor.chain/leave-queue (), :request {:protocol "HTTP/2.0", :async-supported? true, :remote-addr "127.0.0.1", :servlet-response #object[org.eclipse.jetty.ee10.servlet.ServletApiResponse 0x2c7ab3a6 "ServletApiResponse@2c7ab3a6{org.eclipse.jetty.ee10.servlet.ServletContextResponse@b7a9d11,org.eclipse.jetty.ee10.servlet.ServletContextResponse@b7a9d11}"], :servlet #object[io.pedestal.servlet.FnServlet 0x2d6cf3fa "io.pedestal.servlet.FnServlet@2d6cf3fa"], :headers {"accept" "*/*", "accept-encoding" "gzip,deflate", "user-agent" "babashka.http-client/0.4.23"}, :server-port 8133, :servlet-request #object[org.eclipse.jetty.ee10.servlet.ServletApiRequest 0x6f5abaf4 "ServletApiRequest@6f5abaf4{ServletContextRequest@51aa057d{GET@28346b6d  HTTP/2.0}}"], :path-info "/greet", :url-for #delay[], :uri "/greet", :server-name "localhost", :query-string nil, :path-params {}, :body #object[org.eclipse.jetty.ee10.servlet.HttpInput 0x4d430cb5 "HttpInput@1296239797 cs=ServletChannelState@f1383ae{s=HANDLING rs=BLOCKING os=OPEN is=IDLE awp=false se=false i=true al=0} cp=org.eclipse.jetty.ee10.servlet.BlockingContentProducer@4d31f7b0 eof=false"], :scheme :http, :request-method :get, :context-path ""}, :io.pedestal.http.route/path-params-decoded? true, :bindings {#'io.pedestal.http.route/*url-for* #delay[]}, :io.pedestal.interceptor.chain/terminators (#function[io.pedestal.http.response/terminate-when-response*]), :servlet-response #object[org.eclipse.jetty.ee10.servlet.ServletApiResponse 0x2c7ab3a6 "ServletApiResponse@2c7ab3a6{org.eclipse.jetty.ee10.servlet.ServletContextResponse@b7a9d11,org.eclipse.jetty.ee10.servlet.ServletContextResponse@b7a9d11}"], :route {:path "/greet", :method :get, :path-parts ["greet"], :interceptors [#Interceptor{:name :pedestal-stylobate-playground/start-server/greet-handler}], :route-name :greet, :path-params [], :io.pedestal.http.route.internal/satisfies-constraints? #function[io.pedestal.http.route.internal/add-satisfies-constraints?/fn--25274]}, :servlet #object[io.pedestal.servlet.FnServlet 0x2d6cf3fa "io.pedestal.servlet.FnServlet@2d6cf3fa"], :servlet-request #object[org.eclipse.jetty.ee10.servlet.ServletApiRequest 0x6f5abaf4 "ServletApiRequest@6f5abaf4{ServletContextRequest@51aa057d{GET@28346b6d  HTTP/2.0}}"], :websocket-channel-source #object[org.eclipse.jetty.ee10.servlet.ServletApiRequest 0x6f5abaf4 "ServletApiRequest@6f5abaf4{ServletContextRequest@51aa057d{GET@28346b6d  HTTP/2.0}}"], :io.pedestal.interceptor.chain/enter-async [#function[io.pedestal.http.impl.servlet-interceptor/start-servlet-async]], :url-for #delay[], :io.pedestal.interceptor.chain/execution-id 26, :servlet-config #object[org.eclipse.jetty.ee10.servlet.ServletHolder$Config 0x1cfe24d1 "org.eclipse.jetty.ee10.servlet.ServletHolder$Config@1cfe24d1"]}, :line 298}
clojure.lang.ExceptionInfo: clojure.lang.ExceptionInfo in Interceptor :io.pedestal.http.impl.servlet-interceptor/ring-response - Blarpo {:execution-id nil, :stage :leave, :interceptor :io.pedestal.http.impl.servlet-interceptor/ring-response, :exception-type :clojure.lang.ExceptionInfo}
	at io.pedestal.interceptor.chain$throwable__GT_ex_info.invokeStatic(chain.clj:42)
	at io.pedestal.interceptor.chain$throwable__GT_ex_info.invoke(chain.clj:36)
	at io.pedestal.interceptor.chain$begin_error.invokeStatic(chain.clj:74)
	at io.pedestal.interceptor.chain$begin_error.invoke(chain.clj:62)
	at io.pedestal.interceptor.chain$try_stage.invokeStatic(chain.clj:124)
	at io.pedestal.interceptor.chain$try_stage.invoke(chain.clj:108)
	at io.pedestal.interceptor.chain$execute_leave$fn__24994.invoke(chain.clj:271)
	at clojure.lang.AFn.applyToHelper(AFn.java:152)
	at clojure.lang.AFn.applyTo(AFn.java:144)
	at clojure.core$apply.invokeStatic(core.clj:667)
	at clojure.core$with_bindings_STAR_.invokeStatic(core.clj:1990)
	at clojure.core$with_bindings_STAR_.doInvoke(core.clj:1990)
	at clojure.lang.RestFn.invoke(RestFn.java:428)
	at io.pedestal.interceptor.chain$execute_leave.invokeStatic(chain.clj:251)
	at io.pedestal.interceptor.chain$execute_leave.invoke(chain.clj:244)
	at io.pedestal.interceptor.chain$execute_continue.invokeStatic(chain.clj:363)
	at io.pedestal.interceptor.chain$execute_continue.invoke(chain.clj:360)
	at io.pedestal.interceptor.chain$execute.invokeStatic(chain.clj:425)
	at io.pedestal.interceptor.chain$execute.invoke(chain.clj:374)
	at io.pedestal.interceptor.chain$execute.invokeStatic(chain.clj:430)
	at io.pedestal.interceptor.chain$execute.invoke(chain.clj:374)
	at io.pedestal.http.impl.servlet_interceptor$interceptor_service_fn$fn__55567.invoke(servlet_interceptor.clj:386)
	at io.pedestal.servlet.FnServlet.service(FnServlet.java:57)
	at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:614)
	at org.eclipse.jetty.ee10.servlet.ServletHolder.handle(ServletHolder.java:751)
	at org.eclipse.jetty.ee10.servlet.ServletHandler$ChainEnd.doFilter(ServletHandler.java:1622)
	at org.eclipse.jetty.ee10.websocket.servlet.WebSocketUpgradeFilter.doFilter(WebSocketUpgradeFilter.java:195)
	at org.eclipse.jetty.ee10.servlet.FilterHolder.doFilter(FilterHolder.java:205)
	at org.eclipse.jetty.ee10.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1594)
	at org.eclipse.jetty.ee10.servlet.ServletHandler$MappedServlet.handle(ServletHandler.java:1555)
	at org.eclipse.jetty.ee10.servlet.ServletChannel.dispatch(ServletChannel.java:824)
	at org.eclipse.jetty.ee10.servlet.ServletChannel.handle(ServletChannel.java:439)
	at org.eclipse.jetty.ee10.servlet.ServletHandler.handle(ServletHandler.java:470)
	at org.eclipse.jetty.ee10.servlet.SessionHandler.handle(SessionHandler.java:717)
	at org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:1096)
	at org.eclipse.jetty.server.Server.handle(Server.java:182)
	at org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:677)
	at org.eclipse.jetty.util.thread.Invocable$ReadyTask.run(Invocable.java:177)
	at org.eclipse.jetty.http2.server.internal.HttpStreamOverHTTP2$1.run(HttpStreamOverHTTP2.java:144)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.runTask(AdaptiveExecutionStrategy.java:492)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.epcRunTask(AdaptiveExecutionStrategy.java:428)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.consumeTask(AdaptiveExecutionStrategy.java:401)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.tryProduce(AdaptiveExecutionStrategy.java:255)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.produce(AdaptiveExecutionStrategy.java:196)
	at org.eclipse.jetty.http2.HTTP2Connection.produce(HTTP2Connection.java:216)
	at org.eclipse.jetty.http2.HTTP2Connection.onFillable(HTTP2Connection.java:157)
	at org.eclipse.jetty.http2.HTTP2Connection$FillableCallback.succeeded(HTTP2Connection.java:468)
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:105)
	at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.runTask(AdaptiveExecutionStrategy.java:492)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.epcRunTask(AdaptiveExecutionStrategy.java:428)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.consumeTask(AdaptiveExecutionStrategy.java:401)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.tryProduce(AdaptiveExecutionStrategy.java:255)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.run(AdaptiveExecutionStrategy.java:204)
	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:311)
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:981)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1211)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1166)
	at java.base/java.lang.Thread.run(Thread.java:1474)
Caused by: clojure.lang.ExceptionInfo: Blarpo {}
	at pedestal_stylobate_playground$eval57679$fn__57680.invoke(pedestal_stylobate_playground.clj:27)
	at io.pedestal.http.impl.servlet_interceptor$send_response.invokeStatic(servlet_interceptor.clj:196)
	at io.pedestal.http.impl.servlet_interceptor$send_response.invoke(servlet_interceptor.clj:188)
	at io.pedestal.http.impl.servlet_interceptor$leave_ring_response.invokeStatic(servlet_interceptor.clj:256)
	at io.pedestal.http.impl.servlet_interceptor$leave_ring_response.invoke(servlet_interceptor.clj:233)
	at io.pedestal.interceptor.chain$try_stage.invokeStatic(chain.clj:113)
	... 54 common frames omitted
I was expecting a chance to hook my own 500 error response somehow. If I look at the pedestal code, in io.pedestal.http.impl.servlet-interceptor I see:
(defn- leave-stylobate
  [{:keys [^HttpServletRequest servlet-request] :as context}]
  (when (.isAsyncStarted servlet-request)
    (.complete (.getAsyncContext servlet-request)))
  context)
Which is called from:
(defn- error-stylobate
  "Makes sure we send an error response on an exception, even in the
  async case. This is just to make sure exceptions get returned
  somehow; application code should probably catch and log exceptions
  in its own interceptors."
  [error-analyzer context exception]
  (when-let [exception' (error-analyzer context exception)]
    (log/error :msg "error-stylobate triggered"
               :exception exception'
               :context context))
  (leave-stylobate context))
Return of the context means, if I understand correctly, that the error is handled. Am I missing a thing? If not, happy to raise a git issue, if there is interest.

Oh. Maybe stylobate error handling assumes that the connection is likely gone/broken/reset? And that's why a meaningful 500 response isn't worth an attempt to write.