Skip to content

Detect variadic arities of :error interceptors - #795

Open
The-Alchemist wants to merge 1 commit into
metosin:masterfrom
The-Alchemist:pedestal-arities-arglists-variadic
Open

The-Alchemist wants to merge 1 commit into
metosin:masterfrom
The-Alchemist:pedestal-arities-arglists-variadic

Conversation

@The-Alchemist

@The-Alchemist The-Alchemist commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Variadic arities are compiled to doInvoke, not invoke, so they are
invisible here:

f arities before actual
(fn [& _]) #{} 0 or more
(fn [_ & _]) #{} 1 or more
(fn ([_]) ([_ _ & _])) #{1} 1, or 2 or more

The one caller is error-without-arity-2?, so a variadic :error
handler is misread as not supporting 2-arity and gets wrapped by
error-arity-2->1. A handler written as (fn [context ex] ...) via
& args, or one returned by a combinator such as partial/comp,
is silently called with the Sieppari 1-arity convention instead of
Pedestal's 2-arity one.

Change

arities now prefers :arglists metadata when present, and otherwise
combines the declared invoke methods with RestFn.getRequiredArity,
which covers both purely variadic and mixed fixed/variadic fns.

:arglists is checked first because it is the portable description of a
fn's signatures. Class reflection encodes an assumption specific to the
JVM compiler — one class per fn, declaring only the arities it defines.
That does not hold on alternative Clojure runtimes which represent every
fn with one shared class implementing all invoke overloads; there,
arities returns #{0..21} and every :error handler looks like it
accepts any arity. Honouring :arglists gives such runtimes (and anyone
using with-meta) a portable way to report real arities, at no cost on
the JVM where anonymous fns have no metadata and the reflection path
is unchanged.

The single call site now uses a small accepts-arity? helper instead of
contains? on a set, because a set of arities cannot express "n
arguments or more" without inventing an upper bound.

Apologies for the LLM summary!

Motivation

The original motivation is that we are working on a GraalVM fork of Clojure at https://github.com/The-Alchemist/cloffle-clojure which does not generate JVM bytecode so .getDeclaredFields doesn't exist. Adding variadic support does two things at once.

Compatibility

No public API change; arities is private. Behaviour for non-variadic
fns is unchanged, including the existing arities-test assertions.
Variadic :error handlers that accept 2 args are no longer wrapped —
that is the bug fix, and it only affects handlers that were being
called with the wrong convention before.

Opened as a draft — happy to add a CHANGELOG entry or adjust the shape
of the helpers if you would prefer to keep this to a single fn.

@The-Alchemist
The-Alchemist marked this pull request as ready for review September 4, 2026 16:28
@opqdonut

Copy link
Copy Markdown
Member

I had a look at this but I'm unfortunately not familiar with pedestal stuff so I couldn't approve (yet).

@The-Alchemist
The-Alchemist force-pushed the pedestal-arities-arglists-variadic branch from 5e7c2d2 to e73594e Compare September 20, 2026 21:53
@The-Alchemist

Copy link
Copy Markdown
Contributor Author

@opqdonut : here's some LLM analysis, if it helps at all:

PR #795 (pedestal-arities-arglists-variadic) is a small bugfix in how Reitit calls error handlers when it is plugged into Pedestal.

What Pedestal is, in one paragraph

Pedestal is a Clojure HTTP framework. Instead of Ring-style middleware wrapping a handler, it uses interceptors: named steps with optional :enter, :leave, and :error functions that run around a request.

Reitit can replace Pedestal’s router. Reitit’s own interceptors (exceptions, coercion, etc.) come from a different interceptor library, Sieppari. The two models almost match. The mismatch that matters here is error handlers:

  • Pedestal: :error is called with two arguments: the request context and the exception.
  • Sieppari / Reitit defaults: :error is called with one argument: the context, with the exception stuffed into the context map.

When Reitit turns a Reitit interceptor into a Pedestal interceptor, it looks at the :error function and, if it does not take 2 arguments, wraps it so Pedestal’s 2-arg call is turned into Sieppari’s 1-arg call.

The bug

That “does it take 2 arguments?” check used Java reflection on the function’s invoke methods. In Clojure-on-the-JVM, functions like (fn [& args] …) or (fn [ctx & more] …) are compiled to a different method (doInvoke), so reflection reported no arities at all.

The code then assumed “not 2-arity” and always wrapped those handlers. A handler that was actually Pedestal-style (2 args, possibly via &, partial, comp, etc.) got called the Sieppari way instead. There was already a ;; TODO: variadic on that helper.

A second issue: some Clojure runtimes (including a GraalVM-oriented fork) do not give each function its own class with only the real arities. Reflection there can make every function look like it accepts every arity, so the wrap/don’t-wrap decision is also wrong.

What the PR changes

Only reitit.pedestal and its tests. No public API change.

  1. Prefer :arglists metadata when present (the usual Clojure description of a function’s signatures). That works on the JVM and on runtimes without per-fn class reflection.
  2. Otherwise, still use invoke reflection, but also ask Clojure’s RestFn for the variadic required arity.
  3. Ask “can this be called with 2 args?” (including “2 or more” for &) instead of “is 2 in a set of fixed arities?”

Fixed-arity handlers behave as before. Variadic :error handlers that can take 2 arguments are no longer mis-wrapped.

Who cares

If you write Pedestal :error interceptors as (fn [ctx ex] …) you already worked. This matters if the handler is variadic, produced by partial/comp, or you are on a Clojure runtime where function arity cannot be read from generated JVM classes.

Arity detection reflected over the `invoke` methods declared on a fn's
class, which cannot see variadic arities: those are compiled to
`doInvoke`, so a variadic `:error` handler reported no arities at all
and was always treated as 1-arity and wrapped by `error-arity-2->1`.
This was the existing `;; TODO: variadic`.

`signatures` now prefers `:arglists` metadata when present, and
otherwise consults `RestFn.getRequiredArity` alongside the declared
`invoke` methods, so both purely variadic and mixed fixed/variadic fns
are handled.

`:arglists` is checked first because it is the portable description of a
fn's signatures. Class reflection assumes the JVM compiler's one class
per fn with only its declared arities, which does not hold on runtimes
that represent every fn with a single shared class; those declare every
`invoke` overload, making every handler look like it accepts any arity.

Signatures record whether they are variadic rather than collapsing to a
set of arities, since a set cannot express "n arguments or more" without
inventing an upper bound. The one call site, `error-without-arity-2?`,
asks `accepts-arity?` instead of testing set membership.

Signed-off-by: The-Alchemist <kap4020@gmail.com>
@The-Alchemist
The-Alchemist force-pushed the pedestal-arities-arglists-variadic branch from e73594e to 6a37ec6 Compare September 20, 2026 21:58

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants