From be2cef675e74e868acccb09e0acac639c19d5bb4 Mon Sep 17 00:00:00 2001
From: Finnegan's Owner <44065187+pacmano1@users.noreply.github.com>
Date: Tue, 28 Jul 2026 21:24:58 -0600
Subject: [PATCH 1/3] Add configurable request header size to the HTTP Listener
Jetty caps the combined size of all request headers at 8192 bytes and
answers anything larger with 431 Request Header Fields Too Large, before
the channel sees the request. Add a per-connector Request Header Size
setting so that limit can be raised.
The value is a String so it can carry a template, resolved at channel
start the same way host, port and timeout already are. Channels saved
without the element fall back to 8192, so existing channels behave
exactly as before.
A non-positive size is clamped to the default because Jetty treats it as
no limit at all rather than rejecting requests, and the connector panel
cannot vet a channel that arrives through the REST API or an import.
Signed-off-by: Finnegan's Owner <44065187+pacmano1@users.noreply.github.com>
---
.../connect/connectors/http/HttpListener.java | 28 +++-
.../http/DefaultHttpConfiguration.java | 3 +-
.../connect/connectors/http/HttpReceiver.java | 15 ++
.../http/HttpReceiverProperties.java | 18 +++
.../http/DefaultHttpConfigurationTest.java | 115 ++++++++++++++
.../http/HttpReceiverPropertiesTest.java | 66 ++++++++
.../HttpReceiverRequestHeaderSizeTest.java | 142 ++++++++++++++++++
7 files changed, 385 insertions(+), 2 deletions(-)
create mode 100644 server/src/test/java/com/mirth/connect/connectors/http/DefaultHttpConfigurationTest.java
create mode 100644 server/src/test/java/com/mirth/connect/connectors/http/HttpReceiverPropertiesTest.java
create mode 100644 server/src/test/java/com/mirth/connect/connectors/http/HttpReceiverRequestHeaderSizeTest.java
diff --git a/client/src/main/java/com/mirth/connect/connectors/http/HttpListener.java b/client/src/main/java/com/mirth/connect/connectors/http/HttpListener.java
index 38d5d69bf3..c6affba64a 100644
--- a/client/src/main/java/com/mirth/connect/connectors/http/HttpListener.java
+++ b/client/src/main/java/com/mirth/connect/connectors/http/HttpListener.java
@@ -53,6 +53,7 @@
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
import org.apache.http.entity.ContentType;
import org.jdesktop.swingx.decorator.Highlighter;
import org.jdesktop.swingx.decorator.HighlighterFactory;
@@ -142,6 +143,7 @@ public ConnectorProperties getProperties() {
HttpReceiverProperties properties = (HttpReceiverProperties) getDefaults();
properties.setContextPath(contextPathField.getText());
properties.setTimeout(receiveTimeoutField.getText());
+ properties.setRequestHeaderSize(requestHeaderSizeField.getText());
properties.setXmlBody(messageContentXmlBodyRadio.isSelected());
properties.setParseMultipart(parseMultipartYesRadio.isSelected());
properties.setIncludeMetadata(includeMetadataYesRadio.isSelected());
@@ -168,6 +170,7 @@ public void setProperties(ConnectorProperties properties) {
contextPathField.setText(props.getContextPath());
receiveTimeoutField.setText(props.getTimeout());
+ requestHeaderSizeField.setText(props.getRequestHeaderSize());
updateHttpUrl();
@@ -247,6 +250,19 @@ public boolean checkProperties(ConnectorProperties properties, boolean highlight
}
}
+ /*
+ * A template is resolved when the channel starts, so only a plain value can be checked here.
+ * A non-positive size is worth catching because Jetty treats it as no limit at all, which
+ * silently removes the header cap rather than failing visibly.
+ */
+ String requestHeaderSize = props.getRequestHeaderSize();
+ if (!requestHeaderSize.contains("$") && NumberUtils.toInt(requestHeaderSize, 0) <= 0) {
+ valid = false;
+ if (highlight) {
+ requestHeaderSizeField.setBackground(UIConstants.INVALID_COLOR);
+ }
+ }
+
if (!props.getSourceConnectorProperties().getResponseVariable().equalsIgnoreCase("None")) {
if (props.getResponseContentType().length() == 0) {
valid = false;
@@ -269,6 +285,7 @@ public boolean checkProperties(ConnectorProperties properties, boolean highlight
@Override
public void resetInvalidProperties() {
receiveTimeoutField.setBackground(null);
+ requestHeaderSizeField.setBackground(null);
responseContentTypeField.setBackground(null);
responseHeadersVariableField.setBackground(null);
}
@@ -841,6 +858,8 @@ protected void initComponents() {
contextPathField = new MirthTextField();
receiveTimeoutLabel = new JLabel();
receiveTimeoutField = new MirthTextField();
+ requestHeaderSizeLabel = new JLabel();
+ requestHeaderSizeField = new MirthTextField();
httpUrlField = new JTextField();
httpUrlLabel = new JLabel();
headersLabel = new JLabel();
@@ -901,6 +920,8 @@ public void keyReleased(java.awt.event.KeyEvent evt) {
receiveTimeoutLabel.setText("Receive Timeout (ms):");
+ requestHeaderSizeLabel.setText("Request Header Size (bytes):");
+
httpUrlLabel.setText("HTTP URL:");
headersLabel.setText("Response Headers:");
@@ -1042,6 +1063,7 @@ protected void initToolTips() {
charsetEncodingCombobox.setToolTipText(String.format("Select the character set encoding to be used for the response to the sending system.
Set to Default to assume the default character set encoding for the JVM running %s.", BrandingConstants.PRODUCT_NAME));
contextPathField.setToolTipText("The context path for the HTTP Listener URL.");
receiveTimeoutField.setToolTipText("Enter the maximum idle time in milliseconds for a connection.");
+ requestHeaderSizeField.setToolTipText("The maximum combined size in bytes of all request headers.
Requests larger than this are rejected with 431 Request Header Fields
Too Large before the channel sees them. The default is 8192.
The value may include template substitutions, and must resolve to a number.");
httpUrlField.setToolTipText("Displays the generated HTTP URL for the HTTP Listener.");
responseHeadersTable.setToolTipText("Response header parameters are encoded as HTTP headers in the response sent to the client.");
responseStatusCodeField.setToolTipText("Enter the status code for the HTTP response. If this field is left blank a
default status code of 200 will be returned for a successful message,
and 500 will be returned for an errored message. If a \"Respond from\"
value is chosen, that response will be used to determine a successful
or errored response.");
@@ -1061,12 +1083,14 @@ protected void initToolTips() {
}
protected void initLayout() {
- setLayout(new MigLayout("insets 0 8 0 8, novisualpadding, hidemode 3, gap 12 6", "[][]6[]", "[][][][][][][][][][][][][grow][grow]"));
+ setLayout(new MigLayout("insets 0 8 0 8, novisualpadding, hidemode 3, gap 12 6", "[][]6[]", "[][][][][][][][][][][][][][grow][grow]"));
add(contextPathLabel, "right");
add(contextPathField, "w 150!, sx");
add(receiveTimeoutLabel, "newline, right");
add(receiveTimeoutField, "w 100!, sx");
+ add(requestHeaderSizeLabel, "newline, right");
+ add(requestHeaderSizeField, "w 100!, sx");
add(messageContentLabel, "newline, right");
add(messageContentPlainBodyRadio, "split 2");
add(messageContentXmlBodyRadio);
@@ -1229,6 +1253,8 @@ private void useResponseHeadersVariableFieldsEnabled(boolean useTemplate) {
private MirthRadioButton parseMultipartYesRadio;
protected MirthTextField receiveTimeoutField;
protected JLabel receiveTimeoutLabel;
+ protected MirthTextField requestHeaderSizeField;
+ protected JLabel requestHeaderSizeLabel;
protected JLabel responseStatusCodeLabel;
private MirthTextField responseContentTypeField;
private JLabel responseContentTypeLabel;
diff --git a/server/src/main/java/com/mirth/connect/connectors/http/DefaultHttpConfiguration.java b/server/src/main/java/com/mirth/connect/connectors/http/DefaultHttpConfiguration.java
index b69c515a84..bad55cb43f 100644
--- a/server/src/main/java/com/mirth/connect/connectors/http/DefaultHttpConfiguration.java
+++ b/server/src/main/java/com/mirth/connect/connectors/http/DefaultHttpConfiguration.java
@@ -47,7 +47,8 @@ public void configureReceiver(HttpReceiver connector) throws Exception {
org.eclipse.jetty.server.HttpConfiguration httpConfig = new org.eclipse.jetty.server.HttpConfiguration();
httpConfig.setSendServerVersion(false);
httpConfig.setSendXPoweredBy(false);
-
+ httpConfig.setRequestHeaderSize(connector.getRequestHeaderSize());
+
ServerConnector listener = new ServerConnector(connector.getServer(), new HttpConnectionFactory(httpConfig));
listener.setHost(connector.getHost());
listener.setPort(connector.getPort());
diff --git a/server/src/main/java/com/mirth/connect/connectors/http/HttpReceiver.java b/server/src/main/java/com/mirth/connect/connectors/http/HttpReceiver.java
index 4d80a783b6..3af9b77962 100644
--- a/server/src/main/java/com/mirth/connect/connectors/http/HttpReceiver.java
+++ b/server/src/main/java/com/mirth/connect/connectors/http/HttpReceiver.java
@@ -130,6 +130,7 @@ public class HttpReceiver extends SourceConnector implements BinaryContentTypeRe
private String host;
private int port;
private int timeout;
+ private int requestHeaderSize;
private String[] binaryMimeTypesArray;
private Pattern binaryMimeTypesRegex;
private HttpAuthConnectorPluginProperties authProps;
@@ -201,6 +202,16 @@ public void onStart() throws ConnectorTaskException {
host = replacer.replaceValues(getConnectorProperties().getListenerConnectorProperties().getHost(), channelId, channelName);
port = NumberUtils.toInt(replacer.replaceValues(getConnectorProperties().getListenerConnectorProperties().getPort(), channelId, channelName));
timeout = NumberUtils.toInt(replacer.replaceValues(getConnectorProperties().getTimeout(), channelId, channelName), 0);
+ requestHeaderSize = NumberUtils.toInt(replacer.replaceValues(getConnectorProperties().getRequestHeaderSize(), channelId, channelName), HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE);
+
+ /*
+ * Jetty treats a non-positive request header size as no limit at all, so a channel deployed
+ * with one would silently lose the header cap. The connector panel rejects those values, but
+ * a channel imported or pushed through the API never runs that check.
+ */
+ if (requestHeaderSize <= 0) {
+ requestHeaderSize = HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE;
+ }
// Initialize contextPath to "" or its value after replacements
String contextPath = (getConnectorProperties().getContextPath() == null ? "" : replacer.replaceValues(getConnectorProperties().getContextPath(), channelId, channelName)).trim();
@@ -839,6 +850,10 @@ public int getTimeout() {
return timeout;
}
+ public int getRequestHeaderSize() {
+ return requestHeaderSize;
+ }
+
protected Map> extractParameters(Request request) {
Map> parameterMap = new HashMap>();
diff --git a/server/src/main/java/com/mirth/connect/connectors/http/HttpReceiverProperties.java b/server/src/main/java/com/mirth/connect/connectors/http/HttpReceiverProperties.java
index ca667b8e9f..3a3b819a59 100644
--- a/server/src/main/java/com/mirth/connect/connectors/http/HttpReceiverProperties.java
+++ b/server/src/main/java/com/mirth/connect/connectors/http/HttpReceiverProperties.java
@@ -25,6 +25,13 @@
import com.mirth.connect.donkey.util.purge.PurgeUtil;
public class HttpReceiverProperties extends ConnectorProperties implements ListenerConnectorPropertiesInterface, SourceConnectorPropertiesInterface {
+ /**
+ * Jetty's own default for HttpConfiguration.setRequestHeaderSize. Channels saved before this
+ * property existed have no requestHeaderSize element, so the getter falls back to this and
+ * their behavior is unchanged.
+ */
+ public static final int DEFAULT_REQUEST_HEADER_SIZE = 8192;
+
private ListenerConnectorProperties listenerConnectorProperties;
private SourceConnectorProperties sourceConnectorProperties;
@@ -42,6 +49,7 @@ public class HttpReceiverProperties extends ConnectorProperties implements Liste
private String charset;
private String contextPath;
private String timeout;
+ private String requestHeaderSize;
private List staticResources;
public HttpReceiverProperties() {
@@ -60,6 +68,7 @@ public HttpReceiverProperties() {
this.charset = "UTF-8";
this.contextPath = "";
this.timeout = "30000";
+ this.requestHeaderSize = String.valueOf(DEFAULT_REQUEST_HEADER_SIZE);
this.staticResources = new ArrayList();
this.responseHeadersVariable = "";
this.useResponseHeadersVariable = false;
@@ -177,6 +186,14 @@ public void setTimeout(String timeout) {
this.timeout = timeout;
}
+ public String getRequestHeaderSize() {
+ return requestHeaderSize == null ? String.valueOf(DEFAULT_REQUEST_HEADER_SIZE) : requestHeaderSize;
+ }
+
+ public void setRequestHeaderSize(String requestHeaderSize) {
+ this.requestHeaderSize = requestHeaderSize;
+ }
+
public List getStaticResources() {
return staticResources;
}
@@ -287,6 +304,7 @@ public Map getPurgedProperties() {
purgedProperties.put("responseHeaderChars", responseHeaders.size());
purgedProperties.put("charset", charset);
purgedProperties.put("timeout", PurgeUtil.getNumericValue(timeout));
+ purgedProperties.put("requestHeaderSize", PurgeUtil.getNumericValue(getRequestHeaderSize()));
return purgedProperties;
}
}
diff --git a/server/src/test/java/com/mirth/connect/connectors/http/DefaultHttpConfigurationTest.java b/server/src/test/java/com/mirth/connect/connectors/http/DefaultHttpConfigurationTest.java
new file mode 100644
index 0000000000..41b3e8d610
--- /dev/null
+++ b/server/src/test/java/com/mirth/connect/connectors/http/DefaultHttpConfigurationTest.java
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: Open Integration Engine
+
+package com.mirth.connect.connectors.http;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.eclipse.jetty.http.HttpStatus;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.mirth.connect.server.controllers.ConfigurationController;
+import com.mirth.connect.server.controllers.ControllerFactory;
+
+/**
+ * Exercises the request header size setting against a real Jetty connector, since the whole point
+ * of the property is behavior Jetty enforces before any channel code runs.
+ */
+public class DefaultHttpConfigurationTest {
+
+ private static final int LARGE_HEADER_BYTES = 16384;
+
+ private Server server;
+
+ @BeforeClass
+ public static void setupBeforeClass() {
+ ControllerFactory controllerFactory = mock(ControllerFactory.class);
+ when(controllerFactory.createConfigurationController()).thenReturn(mock(ConfigurationController.class));
+
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ requestStaticInjection(ControllerFactory.class);
+ bind(ControllerFactory.class).toInstance(controllerFactory);
+ }
+ });
+ injector.getInstance(ControllerFactory.class);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (server != null) {
+ server.stop();
+ server = null;
+ }
+ }
+
+ @Test
+ public void testJettyDefaultRejectsOversizedHeader() throws Exception {
+ assertEquals(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431, sendLargeHeaderRequest(HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE));
+ }
+
+ @Test
+ public void testRaisedRequestHeaderSizeAcceptsOversizedHeader() throws Exception {
+ assertEquals(HttpStatus.OK_200, sendLargeHeaderRequest(65536));
+ }
+
+ private int sendLargeHeaderRequest(int requestHeaderSize) throws Exception {
+ int port = startReceiver(requestHeaderSize);
+
+ try (CloseableHttpClient client = HttpClients.createDefault()) {
+ HttpGet get = new HttpGet("http://127.0.0.1:" + port + "/");
+ get.addHeader("X-Large-Header", StringUtils.repeat('a', LARGE_HEADER_BYTES));
+
+ try (CloseableHttpResponse response = client.execute(get)) {
+ return response.getStatusLine().getStatusCode();
+ }
+ }
+ }
+
+ private int startReceiver(int requestHeaderSize) throws Exception {
+ server = new Server();
+
+ HttpReceiver receiver = mock(HttpReceiver.class);
+ when(receiver.getServer()).thenReturn(server);
+ when(receiver.getHost()).thenReturn("127.0.0.1");
+ when(receiver.getPort()).thenReturn(0);
+ when(receiver.getTimeout()).thenReturn(30000);
+ when(receiver.getRequestHeaderSize()).thenReturn(requestHeaderSize);
+
+ new DefaultHttpConfiguration().configureReceiver(receiver);
+
+ server.setHandler(new AbstractHandler() {
+ @Override
+ public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
+ baseRequest.setHandled(true);
+ response.setStatus(HttpServletResponse.SC_OK);
+ }
+ });
+
+ server.start();
+ return ((ServerConnector) server.getConnectors()[0]).getLocalPort();
+ }
+}
diff --git a/server/src/test/java/com/mirth/connect/connectors/http/HttpReceiverPropertiesTest.java b/server/src/test/java/com/mirth/connect/connectors/http/HttpReceiverPropertiesTest.java
new file mode 100644
index 0000000000..2ddca72303
--- /dev/null
+++ b/server/src/test/java/com/mirth/connect/connectors/http/HttpReceiverPropertiesTest.java
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: Open Integration Engine
+
+package com.mirth.connect.connectors.http;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.mirth.connect.client.core.Version;
+import com.mirth.connect.model.converters.ObjectXMLSerializer;
+
+public class HttpReceiverPropertiesTest {
+
+ @BeforeClass
+ public static void setup() throws Exception {
+ try {
+ ObjectXMLSerializer.getInstance().init(Version.getLatest().toString());
+ } catch (Exception e) {
+ // Ignore if it has already been initialized
+ }
+ }
+
+ /**
+ * The whole point of the constant is that leaving the field alone changes nothing, so it has to
+ * track Jetty rather than merely agree with itself. This fails if a Jetty upgrade moves the
+ * default out from under us.
+ */
+ @Test
+ public void testDefaultRequestHeaderSizeMatchesJetty() {
+ assertEquals(new org.eclipse.jetty.server.HttpConfiguration().getRequestHeaderSize(), HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE);
+ }
+
+ @Test
+ public void testNewPropertiesUseTheDefaultRequestHeaderSize() {
+ assertEquals(String.valueOf(HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE), new HttpReceiverProperties().getRequestHeaderSize());
+ }
+
+ @Test
+ public void testRequestHeaderSizeRoundTrip() {
+ HttpReceiverProperties properties = new HttpReceiverProperties();
+ properties.setRequestHeaderSize("32768");
+ assertEquals("32768", properties.getRequestHeaderSize());
+ }
+
+ /**
+ * Channels saved before this property existed have no requestHeaderSize element. XStream
+ * instantiates without calling the constructor, so the field stays null and the getter is the
+ * only thing standing between an old channel and a listener configured with a header size of
+ * zero.
+ */
+ @Test
+ public void testPropertiesWithoutRequestHeaderSizeElementFallBackToJettyDefault() {
+ HttpReceiverProperties properties = new HttpReceiverProperties();
+ properties.setRequestHeaderSize("32768");
+
+ String xml = ObjectXMLSerializer.getInstance().serialize(properties);
+ String legacyXml = xml.replaceAll("[^<]*", "");
+ assertFalse(legacyXml.contains("requestHeaderSize"));
+
+ HttpReceiverProperties deserialized = ObjectXMLSerializer.getInstance().deserialize(legacyXml, HttpReceiverProperties.class);
+ assertEquals(String.valueOf(HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE), deserialized.getRequestHeaderSize());
+ }
+}
diff --git a/server/src/test/java/com/mirth/connect/connectors/http/HttpReceiverRequestHeaderSizeTest.java b/server/src/test/java/com/mirth/connect/connectors/http/HttpReceiverRequestHeaderSizeTest.java
new file mode 100644
index 0000000000..110591d617
--- /dev/null
+++ b/server/src/test/java/com/mirth/connect/connectors/http/HttpReceiverRequestHeaderSizeTest.java
@@ -0,0 +1,142 @@
+// SPDX-License-Identifier: MPL-2.0
+// SPDX-FileCopyrightText: Open Integration Engine
+
+package com.mirth.connect.connectors.http;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.UUID;
+
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.mirth.connect.donkey.server.channel.Channel;
+import com.mirth.connect.donkey.server.event.EventDispatcher;
+import com.mirth.connect.server.controllers.ConfigurationController;
+import com.mirth.connect.server.controllers.ControllerFactory;
+import com.mirth.connect.server.controllers.EventController;
+
+/**
+ * Covers what HttpReceiver.onStart() does to the configured request header size before Jetty ever
+ * sees it: template resolution, the fallback for values that cannot be parsed, and the clamp that
+ * keeps a non-positive value from silently removing the header limit.
+ */
+public class HttpReceiverRequestHeaderSizeTest {
+
+ private static final String TEST_CHANNEL_ID = UUID.randomUUID().toString();
+ private static final String TEST_CHANNEL_NAME = "Test HTTP Listener Channel";
+
+ private HttpReceiver receiver;
+
+ @BeforeClass
+ public static void setupBeforeClass() {
+ ControllerFactory controllerFactory = mock(ControllerFactory.class);
+ when(controllerFactory.createConfigurationController()).thenReturn(mock(ConfigurationController.class));
+ when(controllerFactory.createEventController()).thenReturn(mock(EventController.class));
+
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ requestStaticInjection(ControllerFactory.class);
+ bind(ControllerFactory.class).toInstance(controllerFactory);
+ }
+ });
+ injector.getInstance(ControllerFactory.class);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (receiver != null) {
+ receiver.stop();
+ receiver.onUndeploy();
+ receiver = null;
+ }
+ }
+
+ @Test
+ public void testConfiguredValueIsUsed() throws Exception {
+ assertEquals(32768, startWith("32768"));
+ }
+
+ @Test
+ public void testMissingValueFallsBackToJettyDefault() throws Exception {
+ assertEquals(HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE, startWith(null));
+ }
+
+ @Test
+ public void testUnparseableValueFallsBackToJettyDefault() throws Exception {
+ assertEquals(HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE, startWith("not a number"));
+ }
+
+ /**
+ * An unresolved template arrives at NumberUtils as the literal ${...} text, so it has to land on
+ * the default rather than zero.
+ */
+ @Test
+ public void testUnresolvedTemplateFallsBackToJettyDefault() throws Exception {
+ assertEquals(HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE, startWith("${nothingDefinesThis}"));
+ }
+
+ /**
+ * Jetty treats a non-positive request header size as no limit at all. The connector panel rejects
+ * those values, but a channel imported or pushed through the REST API never runs that check, so
+ * the receiver has to clamp them itself.
+ */
+ @Test
+ public void testZeroIsClampedToJettyDefault() throws Exception {
+ assertEquals(HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE, startWith("0"));
+ }
+
+ @Test
+ public void testNegativeIsClampedToJettyDefault() throws Exception {
+ assertEquals(HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE, startWith("-1"));
+ }
+
+ private int startWith(String requestHeaderSize) throws Exception {
+ HttpReceiverProperties properties = new HttpReceiverProperties();
+ properties.getListenerConnectorProperties().setHost("127.0.0.1");
+ properties.getListenerConnectorProperties().setPort("0");
+ properties.setRequestHeaderSize(requestHeaderSize);
+
+ receiver = new TestHttpReceiver(properties);
+
+ Channel channel = new TestChannel();
+ channel.setChannelId(TEST_CHANNEL_ID);
+ channel.setName(TEST_CHANNEL_NAME);
+ receiver.setChannel(channel);
+
+ receiver.onDeploy();
+ receiver.start();
+
+ return receiver.getRequestHeaderSize();
+ }
+
+ private static class TestHttpReceiver extends HttpReceiver {
+
+ public TestHttpReceiver(HttpReceiverProperties properties) {
+ super();
+ setChannelId(TEST_CHANNEL_ID);
+ setMetaDataId(0);
+ setConnectorProperties(properties);
+ }
+
+ @Override
+ protected String getConfigurationClass() {
+ return DefaultHttpConfiguration.class.getName();
+ }
+ }
+
+ private static class TestChannel extends Channel {
+
+ @Override
+ protected EventDispatcher getEventDispatcher() {
+ return mock(EventDispatcher.class);
+ }
+ }
+}
From 7f85f8490a38f09fdaa516ec2d3fa59205dbd3af Mon Sep 17 00:00:00 2001
From: Finnegan's Owner <44065187+pacmano1@users.noreply.github.com>
Date: Fri, 31 Jul 2026 10:12:00 -0600
Subject: [PATCH 2/3] Fail the HTTP Listener on an invalid request header size
A request header size that does not resolve to a positive number now fails
the connector instead of falling back to the default.
Falling back was a silent loss of the limit. Someone setting a cap lower
than the default to bound memory use, or to keep header size within what a
downstream component expects, would have had the higher default quietly
restored. Passing 0 as the NumberUtils default means a value that cannot be
parsed, an unresolved template, zero and a negative all fail through the
same branch, and the message carries the value as it resolved.
Only requestHeaderSize is changed. The pre-existing port, timeout and
responseStatusCode fallbacks in this file have live channels behind them and
tightening those is a separate change.
Signed-off-by: Finnegan's Owner <44065187+pacmano1@users.noreply.github.com>
---
.../connect/connectors/http/HttpReceiver.java | 12 +++--
.../HttpReceiverRequestHeaderSizeTest.java | 49 +++++++++++++------
2 files changed, 42 insertions(+), 19 deletions(-)
diff --git a/server/src/main/java/com/mirth/connect/connectors/http/HttpReceiver.java b/server/src/main/java/com/mirth/connect/connectors/http/HttpReceiver.java
index 3af9b77962..c827dc681d 100644
--- a/server/src/main/java/com/mirth/connect/connectors/http/HttpReceiver.java
+++ b/server/src/main/java/com/mirth/connect/connectors/http/HttpReceiver.java
@@ -202,15 +202,19 @@ public void onStart() throws ConnectorTaskException {
host = replacer.replaceValues(getConnectorProperties().getListenerConnectorProperties().getHost(), channelId, channelName);
port = NumberUtils.toInt(replacer.replaceValues(getConnectorProperties().getListenerConnectorProperties().getPort(), channelId, channelName));
timeout = NumberUtils.toInt(replacer.replaceValues(getConnectorProperties().getTimeout(), channelId, channelName), 0);
- requestHeaderSize = NumberUtils.toInt(replacer.replaceValues(getConnectorProperties().getRequestHeaderSize(), channelId, channelName), HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE);
/*
- * Jetty treats a non-positive request header size as no limit at all, so a channel deployed
- * with one would silently lose the header cap. The connector panel rejects those values, but
+ * A request header size that does not resolve to a positive number fails the connector
+ * rather than falling back to the default. Jetty treats a non-positive size as no limit at
+ * all, and a value that cannot be parsed would quietly restore a cap the user was trying to
+ * lower, so both cases are a silent loss of the limit. The connector panel rejects them, but
* a channel imported or pushed through the API never runs that check.
*/
+ String requestHeaderSizeValue = replacer.replaceValues(getConnectorProperties().getRequestHeaderSize(), channelId, channelName);
+ requestHeaderSize = NumberUtils.toInt(requestHeaderSizeValue, 0);
+
if (requestHeaderSize <= 0) {
- requestHeaderSize = HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE;
+ throw new ConnectorTaskException("Invalid request header size: " + requestHeaderSizeValue);
}
// Initialize contextPath to "" or its value after replacements
diff --git a/server/src/test/java/com/mirth/connect/connectors/http/HttpReceiverRequestHeaderSizeTest.java b/server/src/test/java/com/mirth/connect/connectors/http/HttpReceiverRequestHeaderSizeTest.java
index 110591d617..8d373cf315 100644
--- a/server/src/test/java/com/mirth/connect/connectors/http/HttpReceiverRequestHeaderSizeTest.java
+++ b/server/src/test/java/com/mirth/connect/connectors/http/HttpReceiverRequestHeaderSizeTest.java
@@ -4,6 +4,8 @@
package com.mirth.connect.connectors.http;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -16,6 +18,7 @@
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
+import com.mirth.connect.donkey.server.ConnectorTaskException;
import com.mirth.connect.donkey.server.channel.Channel;
import com.mirth.connect.donkey.server.event.EventDispatcher;
import com.mirth.connect.server.controllers.ConfigurationController;
@@ -23,9 +26,9 @@
import com.mirth.connect.server.controllers.EventController;
/**
- * Covers what HttpReceiver.onStart() does to the configured request header size before Jetty ever
- * sees it: template resolution, the fallback for values that cannot be parsed, and the clamp that
- * keeps a non-positive value from silently removing the header limit.
+ * Covers what HttpReceiver.onStart() does with the configured request header size before Jetty ever
+ * sees it: template resolution, the default applied when nothing is configured, and the failure
+ * raised for anything that does not resolve to a positive number.
*/
public class HttpReceiverRequestHeaderSizeTest {
@@ -69,33 +72,49 @@ public void testMissingValueFallsBackToJettyDefault() throws Exception {
assertEquals(HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE, startWith(null));
}
+ /**
+ * A value that cannot be parsed would otherwise restore a cap the user was trying to lower, so it
+ * fails the connector rather than falling back.
+ */
@Test
- public void testUnparseableValueFallsBackToJettyDefault() throws Exception {
- assertEquals(HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE, startWith("not a number"));
+ public void testUnparseableValueFailsToStart() throws Exception {
+ assertFailsToStart("not a number");
}
/**
- * An unresolved template arrives at NumberUtils as the literal ${...} text, so it has to land on
- * the default rather than zero.
+ * An unresolved template arrives at NumberUtils as the literal ${...} text. Failing tells the user
+ * their substitution is broken instead of quietly running on the default.
*/
@Test
- public void testUnresolvedTemplateFallsBackToJettyDefault() throws Exception {
- assertEquals(HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE, startWith("${nothingDefinesThis}"));
+ public void testUnresolvedTemplateFailsToStart() throws Exception {
+ assertFailsToStart("${nothingDefinesThis}");
}
/**
* Jetty treats a non-positive request header size as no limit at all. The connector panel rejects
- * those values, but a channel imported or pushed through the REST API never runs that check, so
- * the receiver has to clamp them itself.
+ * those values, but a channel imported or pushed through the REST API never runs that check.
*/
@Test
- public void testZeroIsClampedToJettyDefault() throws Exception {
- assertEquals(HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE, startWith("0"));
+ public void testZeroFailsToStart() throws Exception {
+ assertFailsToStart("0");
}
@Test
- public void testNegativeIsClampedToJettyDefault() throws Exception {
- assertEquals(HttpReceiverProperties.DEFAULT_REQUEST_HEADER_SIZE, startWith("-1"));
+ public void testNegativeFailsToStart() throws Exception {
+ assertFailsToStart("-1");
+ }
+
+ /**
+ * Asserts the connector refuses to start and that the message carries the value as it resolved, so
+ * the cause is visible without reading the channel configuration.
+ */
+ private void assertFailsToStart(String requestHeaderSize) throws Exception {
+ try {
+ startWith(requestHeaderSize);
+ fail("Expected ConnectorTaskException for request header size \"" + requestHeaderSize + "\"");
+ } catch (ConnectorTaskException e) {
+ assertTrue("Message should contain the resolved value but was: " + e.getMessage(), e.getMessage().contains(requestHeaderSize));
+ }
}
private int startWith(String requestHeaderSize) throws Exception {
From 0e4ffd0824b7b426e1e1d03601d093f6c90020de Mon Sep 17 00:00:00 2001
From: Finnegan's Owner <44065187+pacmano1@users.noreply.github.com>
Date: Sat, 1 Aug 2026 13:57:49 -0600
Subject: [PATCH 3/3] Extract the Jetty listener configuration to a protected
method
configureReceiver built the Jetty HttpConfiguration inline, so an
implementation overriding it had to repeat those settings and would not pick
up later additions to them.
Moving construction into a protected createHttpConfig lets a subclass build
its listener from the current defaults instead. TLSHttpConfiguration in the
NovaMap TLS manager plugin is the case in point: it extends this class, and
its TLS branch already repeats setSendServerVersion and setSendXPoweredBy
while missing the request header size entirely.
No behaviour change.
Signed-off-by: Finnegan's Owner <44065187+pacmano1@users.noreply.github.com>
---
.../http/DefaultHttpConfiguration.java | 20 +++++++++++++------
1 file changed, 14 insertions(+), 6 deletions(-)
diff --git a/server/src/main/java/com/mirth/connect/connectors/http/DefaultHttpConfiguration.java b/server/src/main/java/com/mirth/connect/connectors/http/DefaultHttpConfiguration.java
index bad55cb43f..608e8eaa03 100644
--- a/server/src/main/java/com/mirth/connect/connectors/http/DefaultHttpConfiguration.java
+++ b/server/src/main/java/com/mirth/connect/connectors/http/DefaultHttpConfiguration.java
@@ -44,18 +44,26 @@ public void configureConnectorUndeploy(Connector connector) {}
@Override
public void configureReceiver(HttpReceiver connector) throws Exception {
- org.eclipse.jetty.server.HttpConfiguration httpConfig = new org.eclipse.jetty.server.HttpConfiguration();
- httpConfig.setSendServerVersion(false);
- httpConfig.setSendXPoweredBy(false);
- httpConfig.setRequestHeaderSize(connector.getRequestHeaderSize());
-
- ServerConnector listener = new ServerConnector(connector.getServer(), new HttpConnectionFactory(httpConfig));
+ ServerConnector listener = new ServerConnector(connector.getServer(), new HttpConnectionFactory(createHttpConfig(connector)));
listener.setHost(connector.getHost());
listener.setPort(connector.getPort());
listener.setIdleTimeout(connector.getTimeout());
connector.getServer().addConnector(listener);
}
+ /**
+ * Builds the Jetty configuration the listener is created with. An implementation that overrides
+ * {@link #configureReceiver(HttpReceiver)} should build its own listener from this rather than
+ * repeating the settings, so that it keeps up with changes to the defaults.
+ */
+ protected org.eclipse.jetty.server.HttpConfiguration createHttpConfig(HttpReceiver connector) {
+ org.eclipse.jetty.server.HttpConfiguration httpConfig = new org.eclipse.jetty.server.HttpConfiguration();
+ httpConfig.setSendServerVersion(false);
+ httpConfig.setSendXPoweredBy(false);
+ httpConfig.setRequestHeaderSize(connector.getRequestHeaderSize());
+ return httpConfig;
+ }
+
@Override
public void configureDispatcher(HttpDispatcher connector, HttpDispatcherProperties connectorProperties) throws Exception {}