Standard Servlet specification (Jakarta Servlet 6.0) and Tomcat Catalina container APIs + * enforce a strict HTTP response lifecycle (Status Line → Headers → Response Body) via + * {@link jakarta.servlet.http.HttpServletResponse} and {@link jakarta.servlet.ServletOutputStream}. + * Attempting to write bytes via standard output streams will treat those bytes as part of the + * response body after committing status code 200, rather than emitting preliminary 1xx + * informational response lines (such as {@code HTTP/1.1 104 Resumption}). + * + *
While Tomcat provides internal APIs like {@code Response#sendAcknowledgement()} (via Coyote + * {@code ActionCode.ACK}), that mechanism is hardcoded exclusively for HTTP 100 Continue frames. + * Tomcat 10 / Servlet 6.0 does not expose a public, non-reflective API for writing arbitrary raw + * 1xx interim status frames directly to the TCP socket before servlet dispatch. + * + *
To emit raw HTTP 104 frames, this valve retrieves Tomcat's low-level {@code SocketWrapperBase} + * from Coyote's internal {@code Http11OutputBuffer}. To eliminate per-request reflection overhead, + * target {@link Field} and {@link Method} instances are resolved and cached statically upon first + * execution. + * + *
ponytail: Tomcat encapsulates SocketWrapperBase inside Http11OutputBuffer without exposing a
+ * public raw 1xx writing API in Tomcat 10. Reflection lookups are cached to eliminate per-request
+ * reflection overhead. Upgrade path: Replace with Tomcat 12+ sendEarlyHints / Servlet 6.2 1xx
+ * APIs once standard.
+ *
+ * @param response Tomcat's Catalina Response connector object
+ * @param bytes the UTF-8 encoded HTTP 104 frame bytes to write
+ * @return true if successfully written and flushed to the socket; false otherwise
+ */
+ private boolean writeToSocketWrapper(Response response, byte[] bytes) {
+ try {
+ // Access low-level Coyote response object
+ org.apache.coyote.Response coyoteResponse = response.getCoyoteResponse();
+
+ // Extract low-level output buffer object (e.g. Http11OutputBuffer)
+ Object outputBuffer = getOutputBuffer(coyoteResponse);
+ if (outputBuffer != null) {
+
+ // Extract SocketWrapperBase instance from output buffer
+ Object socketWrapper = getSocketWrapper(outputBuffer);
+ if (socketWrapper != null) {
+
+ // Invoke socketWrapper.write(...) and socketWrapper.flush(...)
+ return invokeWriteAndFlush(socketWrapper, bytes);
+ }
+ }
+ } catch (Exception e) {
+ LOG.warn("Failed to write to Tomcat SocketWrapperBase via reflection", e);
+ }
+ return false;
+ }
+
+ /**
+ * Resolves and caches the private {@code outputBuffer} field on {@link
+ * org.apache.coyote.Response}.
+ *
+ * @param coyoteResponse the low-level Coyote response instance
+ * @return the internal output buffer object, or null if unresolvable
+ * @throws Exception if reflection access fails
+ */
+ private Object getOutputBuffer(org.apache.coyote.Response coyoteResponse) throws Exception {
+ if (outputBufferField == null) {
+ Field field = org.apache.coyote.Response.class.getDeclaredField("outputBuffer");
+ field.setAccessible(true);
+ outputBufferField = field;
+ }
+ return outputBufferField.get(coyoteResponse);
+ }
+
+ /**
+ * Traverses the class hierarchy of the output buffer to resolve and cache the {@code
+ * socketWrapper} field.
+ *
+ * @param outputBuffer the output buffer instance (e.g., Http11OutputBuffer)
+ * @return the SocketWrapperBase instance, or null if unresolvable
+ * @throws Exception if reflection access fails
+ */
+ private Object getSocketWrapper(Object outputBuffer) throws Exception {
+ if (socketWrapperField == null
+ || !socketWrapperField.getDeclaringClass().isAssignableFrom(outputBuffer.getClass())) {
+ Field field = findDeclaredField(outputBuffer.getClass(), "socketWrapper");
+ if (field != null) {
+ field.setAccessible(true);
+ socketWrapperField = field;
+ }
+ }
+ return socketWrapperField != null ? socketWrapperField.get(outputBuffer) : null;
+ }
+
+ /**
+ * Resolves and invokes {@code write(boolean block, byte[] b, int off, int len)} and {@code
+ * flush(boolean block)} on Tomcat's low-level {@code SocketWrapperBase}.
+ *
+ * @param socketWrapper the SocketWrapperBase instance
+ * @param bytes the raw HTTP bytes to transmit
+ * @return true if write and flush succeeded
+ * @throws Exception if reflection invocation fails
+ */
+ private boolean invokeWriteAndFlush(Object socketWrapper, byte[] bytes) throws Exception {
+ if (writeMethod == null) {
+ // Resolve SocketWrapperBase.write(boolean block, byte[] b, int off, int len)
+ writeMethod =
+ socketWrapper
+ .getClass()
+ .getMethod("write", boolean.class, byte[].class, int.class, int.class);
+ }
+ if (flushMethod == null) {
+ // Resolve SocketWrapperBase.flush(boolean block)
+ flushMethod = socketWrapper.getClass().getMethod("flush", boolean.class);
+ }
+
+ // Write bytes synchronously (block = true) directly into socket buffer
+ writeMethod.invoke(socketWrapper, true, bytes, 0, bytes.length);
+
+ // Flush socket buffer immediately to send raw 104 frame across the wire
+ flushMethod.invoke(socketWrapper, true);
+ return true;
+ }
+
+ /**
+ * Helper utility to search for a declared field up through a class inheritance hierarchy.
+ *
+ * @param clazz the target class
+ * @param fieldName the name of the field to find
+ * @return the Field instance if found, or null
+ */
+ private static Field findDeclaredField(Class> clazz, String fieldName) {
+ Class> current = clazz;
+ while (current != null) {
+ try {
+ return current.getDeclaredField(fieldName);
+ } catch (NoSuchFieldException e) {
+ current = current.getSuperclass();
+ }
+ }
+ return null;
+ }
+}
diff --git a/spring-boot-rest/src/main/java/me/desair/spring/tus/WebMvcConfig.java b/spring-boot-rest/src/main/java/me/desair/spring/tus/WebMvcConfig.java
index 8ba79bb..697a3b8 100644
--- a/spring-boot-rest/src/main/java/me/desair/spring/tus/WebMvcConfig.java
+++ b/spring-boot-rest/src/main/java/me/desair/spring/tus/WebMvcConfig.java
@@ -1,26 +1,23 @@
package me.desair.spring.tus;
-import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Profile;
-import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
-import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
- private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/public/" };
+ private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {"classpath:/public/"};
- @Override
- public void addResourceHandlers(ResourceHandlerRegistry registry) {
- if (!registry.hasMappingForPattern("/webjars/**")) {
- registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
- }
- if (!registry.hasMappingForPattern("/**")) {
- registry.addResourceHandler("/**").addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS);
- }
+ @Override
+ public void addResourceHandlers(ResourceHandlerRegistry registry) {
+ if (!registry.hasMappingForPattern("/webjars/**")) {
+ registry
+ .addResourceHandler("/webjars/**")
+ .addResourceLocations("classpath:/META-INF/resources/webjars/");
}
-
+ if (!registry.hasMappingForPattern("/**")) {
+ registry.addResourceHandler("/**").addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS);
+ }
+ }
}
diff --git a/spring-boot-rest/src/main/java/me/desair/spring/tus/client/UploadScript.java b/spring-boot-rest/src/main/java/me/desair/spring/tus/client/UploadScript.java
index 3c644c6..fd3cb2a 100644
--- a/spring-boot-rest/src/main/java/me/desair/spring/tus/client/UploadScript.java
+++ b/spring-boot-rest/src/main/java/me/desair/spring/tus/client/UploadScript.java
@@ -1,82 +1,83 @@
package me.desair.spring.tus.client;
-import java.io.File;
-import java.io.IOException;
-import java.net.URL;
-
import io.tus.java.client.ProtocolException;
import io.tus.java.client.TusClient;
import io.tus.java.client.TusExecutor;
import io.tus.java.client.TusURLMemoryStore;
import io.tus.java.client.TusUpload;
import io.tus.java.client.TusUploader;
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
public class UploadScript {
- public static void main(String args[]) throws IOException, ProtocolException {
- if(args == null || args.length != 1) {
- System.err.println("Usage: me.desair.spring.tus.client.UploadScript