You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[BUG] A request the HTTP Listener rejects leaves no record anywhere #480
Describe the bug
This is the HTTP Listener source connector. When the Jetty server it runs rejects a request before the channel sees it, nothing is recorded: no message, no event, nothing in mirth.log. Same for a header that is too large (431), a URI that is too long (414) and a malformed header (400). Reproducible every time.
To Reproduce
Deploy an HTTP Listener with the default Request Header Size (8192).
The client gets 431 Request Header Fields Too Large.
Look at the dashboard, the Events tab and logs/mirth.log.
Expected behavior
An operator can see that the listener turned a request away, and why.
Actual behavior
Nothing anywhere. No event is written. Jetty logs a WARN, but conf/log4j2.properties sets rootLogger = ERROR with no Jetty logger, so the line is dropped.
Environment
OIE 4.6.0 (Linux tarball), OpenJDK 17.0.20.1, Jetty 9.4.57. main has the same Jetty version and the same log4j2.properties root logger.
Workaround(s)
Add to conf/log4j2.properties and restart:
A 431 then writes WARN ... org.eclipse.jetty.http.HttpParser: Header is too large 8193>8192 to mirth.log, and a 414 writes URI is too large >8192. The line names no channel, port or client, a 400 for a malformed header still logs nothing, and there is still no event.
Additional context
Which Jetty hooks see a rejected request, checked on 9.4.57:
HttpChannel.Listener.onRequestFailure fires with BadMessageException: 431 (register with connector.addBean(...)). It is the only one that reports the failure.
ErrorHandler.handle never fires on this path.
RequestLog.log, onResponseBegin and onComplete fire but report status 200, so a request log would record the rejection as a success.
HttpReceiver already dispatches ErrorEvents at four sites, so an Events entry from onRequestFailure would fit. Jetty throws BadMessageException for the 400 and 414 cases too, so the same hook should cover them (not tested).
I'd like to pick this up. Here is the approach, so we can agree on it before I open a draft PR.
What I confirmed first (Jetty 9.4.57, main at eed3361)
A throwaway probe with a listener registered via connector.addBean(...):
Request
Client gets
HttpChannel.Listener.onRequestFailure
9,000-byte header
431
fires, BadMessageException
URI over 8,192
414
fires, BadMessageException
Malformed header line
400
fires, HttpParser.IllegalCharacterException (a BadMessageException subclass)
Bad request line
400
fires, BadMessageException
Normal request
200
does not fire
So one instanceof BadMessageException check covers all four, including the malformed-header case the issue had not tested. The request passed to the hook carries the client address and local port.
Design
Two small pieces, so the Jetty-specific part stays tiny:
RejectedRequestRecorder (no Jetty imports). Given status, reason, client address and local port, it writes the log line and dispatches the event for one HTTP Listener. All behaviour lives here and is unit tested without a server.
A Jetty adapter: an HttpChannel.Listener whose onRequestFailure checks for BadMessageException and calls the recorder. HttpReceiver.onStart() registers it on every connector after configuration.configureReceiver(this) returns, so configuration classes that build their own ServerConnector, such as an HTTPS configuration, are covered too.
Jetty 12 removes HttpChannel.Listener and handles bad messages on a different path, so when #352 moves us off 9.4 only the adapter has to be rewritten against whichever Jetty 12 hook sees these failures. The recorder and its tests stay as they are.
What it records
An ErrorEvent of type SOURCE_CONNECTOR, built the same way as the existing ErrorEvent sites in HttpReceiver and TcpReceiver, with a message such as Request rejected before reaching the channel: 431 Request Header Fields Too Large (client 10.0.0.5, port 8081). No throwable is attached, since the stack trace adds nothing.
A log line from HttpReceiver at ERROR, matching the other ErrorEvent sites. The shipped log4j2.properties has the root logger at ERROR, so a WARN would be invisible by default.
Never the request URI or header values. A 414 URI or an oversized header can carry patient data in a query string or token, and it is the oversized part by definition.
Nothing changes in what the client receives.
Volume, and the effect on alerts
DefaultAlertWorker turns ErrorEvents into alerts, so anyone with an alert on Source Connector or Any errors would start getting one per rejected request. A port scanner could send hundreds a minute.
My proposal: per HTTP Listener and per status code, record the first rejection at once, then at most one event and one log line per 60 seconds, with the text giving how many were rejected in that window (for example ... 431 Request Header Fields Too Large, 37 times in the last 60 s). The counter lives in the recorder, resets when the connector stops, and needs no new settings. A configurable window could come later if someone asks for it.
If you would rather match TcpReceiver exactly and record every rejection, that is a one-line change in the recorder, and I'll do it that way.
Tests
Recorder unit tests (no server, clock injected so the 60-second window is tested without sleeping):
Each status gives one event with the right type, status, reason, client and port.
A second rejection inside the window gives no event; the first one after the window carries the count.
Different status codes, and different listeners, keep separate windows.
Rejections arriving on many threads at once give exactly one event per window, and the count adds up.
The message never contains the URI or header text.
Stopping the connector resets the counters.
HttpReceiver tests on a real listener on port 0, in the style of HttpReceiverRequestHeaderSizeTest:
Send the four bad requests above and one normal one. The client still gets 431, 414, 400 and 400, each bad request dispatches its event, and the normal request dispatches none.
A test configuration class that builds its own ServerConnector (as an HTTPS configuration does) still gets the hook.
After stop and start, a new rejection is recorded at once.
End to end, before marking the PR ready:
Full ./gradlew build and ./gradlew test pass locally and in CI.
Run the server with ./gradlew :server:devRun, deploy an HTTP Listener channel, send the oversized header with curl, and attach a screenshot of the entry in the Administrator's Events tab and the line in mirth.log.
With an alert on Source Connector errors, confirm a burst of 100 rejections produces one alert, not 100.
Out of scope, as follow-ups if wanted
The Administrator and API server (MirthWebServer) is also Jetty and has the same blind spot. It could reuse the recorder later.
The Web Service Listener uses the JDK HttpServer, not Jetty, so it is not affected by this issue.
No change to conf/log4j2.properties.
Does this match what you had in mind, in particular the 60-second rule?
Activity
jitendrachoudhary9623 commentedon Oct 26, 2026
I'd like to pick this up. Here is the approach, so we can agree on it before I open a draft PR.
What I confirmed first (Jetty 9.4.57, main at eed3361)
A throwaway probe with a listener registered via
connector.addBean(...):HttpChannel.Listener.onRequestFailureBadMessageExceptionBadMessageExceptionHttpParser.IllegalCharacterException(aBadMessageExceptionsubclass)BadMessageExceptionSo one
instanceof BadMessageExceptioncheck covers all four, including the malformed-header case the issue had not tested. The request passed to the hook carries the client address and local port.Design
Two small pieces, so the Jetty-specific part stays tiny:
RejectedRequestRecorder(no Jetty imports). Given status, reason, client address and local port, it writes the log line and dispatches the event for one HTTP Listener. All behaviour lives here and is unit tested without a server.HttpChannel.ListenerwhoseonRequestFailurechecks forBadMessageExceptionand calls the recorder.HttpReceiver.onStart()registers it on every connector afterconfiguration.configureReceiver(this)returns, so configuration classes that build their ownServerConnector, such as an HTTPS configuration, are covered too.Jetty 12 removes
HttpChannel.Listenerand handles bad messages on a different path, so when #352 moves us off 9.4 only the adapter has to be rewritten against whichever Jetty 12 hook sees these failures. The recorder and its tests stay as they are.What it records
ErrorEventof typeSOURCE_CONNECTOR, built the same way as the existingErrorEventsites inHttpReceiverandTcpReceiver, with a message such asRequest rejected before reaching the channel: 431 Request Header Fields Too Large (client 10.0.0.5, port 8081). No throwable is attached, since the stack trace adds nothing.HttpReceiverat ERROR, matching the otherErrorEventsites. The shippedlog4j2.propertieshas the root logger at ERROR, so a WARN would be invisible by default.Nothing changes in what the client receives.
Volume, and the effect on alerts
DefaultAlertWorkerturnsErrorEvents into alerts, so anyone with an alert on Source Connector or Any errors would start getting one per rejected request. A port scanner could send hundreds a minute.My proposal: per HTTP Listener and per status code, record the first rejection at once, then at most one event and one log line per 60 seconds, with the text giving how many were rejected in that window (for example
... 431 Request Header Fields Too Large, 37 times in the last 60 s). The counter lives in the recorder, resets when the connector stops, and needs no new settings. A configurable window could come later if someone asks for it.If you would rather match
TcpReceiverexactly and record every rejection, that is a one-line change in the recorder, and I'll do it that way.Tests
Recorder unit tests (no server, clock injected so the 60-second window is tested without sleeping):
HttpReceiver tests on a real listener on port 0, in the style of
HttpReceiverRequestHeaderSizeTest:ServerConnector(as an HTTPS configuration does) still gets the hook.End to end, before marking the PR ready:
./gradlew buildand./gradlew testpass locally and in CI../gradlew :server:devRun, deploy an HTTP Listener channel, send the oversized header with curl, and attach a screenshot of the entry in the Administrator's Events tab and the line in mirth.log.Out of scope, as follow-ups if wanted
MirthWebServer) is also Jetty and has the same blind spot. It could reuse the recorder later.HttpServer, not Jetty, so it is not affected by this issue.conf/log4j2.properties.Does this match what you had in mind, in particular the 60-second rule?
pacmano1 commentedon Oct 26, 2026
@jitendrachoudhary9623 - just open a PR.