Skip to content

Commit 77b3b82

Browse files
authored
Merge pull request #42 from FusionAuth/degroff/catch_socket_reset_exception
Add support to bind an unexpected exception handler
2 parents 476cccb + 4d90578 commit 77b3b82

File tree

9 files changed

+207
-19
lines changed

9 files changed

+207
-19
lines changed

build.savant

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ restifyVersion = "4.2.1"
1818
slf4jVersion = "2.0.17"
1919
testngVersion = "7.11.0"
2020

21-
project(group: "io.fusionauth", name: "java-http", version: "1.1.2", licenses: ["ApacheV2_0"]) {
21+
project(group: "io.fusionauth", name: "java-http", version: "1.2.0", licenses: ["ApacheV2_0"]) {
2222
workflow {
2323
fetch {
2424
// Dependency resolution order:

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<modelVersion>4.0.0</modelVersion>
33
<groupId>io.fusionauth</groupId>
44
<artifactId>java-http</artifactId>
5-
<version>1.1.2</version>
5+
<version>1.2.0</version>
66
<packaging>jar</packaging>
77

88
<name>Java HTTP library (client and server)</name>

src/main/java/io/fusionauth/http/server/Configurable.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,23 @@ default T withShutdownDuration(Duration duration) {
331331
return (T) this;
332332
}
333333

334+
/**
335+
*
336+
* Sets the unexpected exception handler. This handler will be called when an unexpected exception is taken while processing the HTTP
337+
* request by the HTTP worker.
338+
* <p>
339+
* This allows you to customize the status code and logging behavior.
340+
* <p>
341+
* Must not be null.
342+
*
343+
* @param unexpectedExceptionHandler The unexpected exception handler.
344+
* @return This.
345+
*/
346+
default T withUnexpectedExceptionHandler(HTTPUnexpectedExceptionHandler unexpectedExceptionHandler) {
347+
configuration().withUnexpectedExceptionHandler(unexpectedExceptionHandler);
348+
return (T) this;
349+
}
350+
334351
/**
335352
* This configures the duration of the initial delay before calculating and enforcing the minimum write throughput. Defaults to 5
336353
* seconds.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (c) 2025, FusionAuth, All Rights Reserved
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing,
11+
* software distributed under the License is distributed on an
12+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13+
* either express or implied. See the License for the specific
14+
* language governing permissions and limitations under the License.
15+
*/
16+
package io.fusionauth.http.server;
17+
18+
/**
19+
* THe default HTTP unexpected exception handler.
20+
*
21+
* @author Daniel DeGroff
22+
*/
23+
public class DefaultHTTPUnexpectedExceptionHandler implements HTTPUnexpectedExceptionHandler {
24+
@Override
25+
public void handle(ExceptionHandlerContext context) {
26+
context.getLogger()
27+
.error(String.format("[%s] Closing socket with status [%d]. An HTTP worker threw an exception while processing a request.",
28+
Thread.currentThread().threadId(),
29+
context.getStatusCode()),
30+
context.getThrowable());
31+
}
32+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright (c) 2025, FusionAuth, All Rights Reserved
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing,
11+
* software distributed under the License is distributed on an
12+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13+
* either express or implied. See the License for the specific
14+
* language governing permissions and limitations under the License.
15+
*/
16+
package io.fusionauth.http.server;
17+
18+
import io.fusionauth.http.log.Logger;
19+
20+
/**
21+
* Provide context to the exception handler.
22+
*
23+
* @author Daniel DeGroff
24+
*/
25+
public class ExceptionHandlerContext {
26+
private final Logger logger;
27+
28+
private final HTTPRequest request;
29+
30+
private final Throwable throwable;
31+
32+
private int statusCode;
33+
34+
public ExceptionHandlerContext(Logger logger, HTTPRequest request, int statusCode, Throwable throwable) {
35+
this.logger = logger;
36+
this.request = request;
37+
this.statusCode = statusCode;
38+
this.throwable = throwable;
39+
}
40+
41+
/**
42+
* This is provided for convenience, but you may wish to use your own logger.
43+
*
44+
* @return the optional logger to use in the exception handler.
45+
*/
46+
public Logger getLogger() {
47+
return logger;
48+
}
49+
50+
/**
51+
* This may be useful if you wish to know additional context of the exception such as the URI of the current HTTP request.
52+
* <p>
53+
* Modifications to this object will have no effect on current or futures requests.
54+
*
55+
* @return the current HTTP request, or null if this exception was taking prior to constructing the HTTP request. This is unlikely but
56+
* please account for this value being null.
57+
*/
58+
public HTTPRequest getRequest() {
59+
return request;
60+
}
61+
62+
/**
63+
* @return the desired status code for the HTTP response.
64+
*/
65+
public int getStatusCode() {
66+
return statusCode;
67+
}
68+
69+
/**
70+
* Suggest a status code for the HTTP response. This value will be used unless the response has already been committed meaning bytes have
71+
* already been written to the client and the HTTP server is not able to modify the response code.
72+
*
73+
* @param statusCode the desired status code to set on the HTTP response.
74+
*/
75+
public void setStatusCode(int statusCode) {
76+
this.statusCode = statusCode;
77+
}
78+
79+
/**
80+
* @return the unexpected exception to handle
81+
*/
82+
public Throwable getThrowable() {
83+
return throwable;
84+
}
85+
}

src/main/java/io/fusionauth/http/server/HTTPServerConfiguration.java

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ public class HTTPServerConfiguration implements Configurable<HTTPServerConfigura
7979

8080
private Duration shutdownDuration = Duration.ofSeconds(10);
8181

82+
private HTTPUnexpectedExceptionHandler unexpectedExceptionHandler = new DefaultHTTPUnexpectedExceptionHandler();
83+
8284
private Duration writeThroughputCalculationDelayDuration = Duration.ofSeconds(5);
8385

8486
/**
@@ -275,6 +277,13 @@ public Duration getShutdownDuration() {
275277
return shutdownDuration;
276278
}
277279

280+
/**
281+
* @return The HTTP unexpected exception handler for this server. Never null.
282+
*/
283+
public HTTPUnexpectedExceptionHandler getUnexpectedExceptionHandler() {
284+
return unexpectedExceptionHandler;
285+
}
286+
278287
/**
279288
* @return the duration that will be used to delay the calculation and enforcement of the minimum write throughput.
280289
*/
@@ -334,7 +343,7 @@ public HTTPServerConfiguration withContextPath(String contextPath) {
334343
*/
335344
@Override
336345
public HTTPServerConfiguration withExpectValidator(ExpectValidator validator) {
337-
Objects.requireNonNull(handler, "You cannot set ExpectValidator to null");
346+
Objects.requireNonNull(validator, "You cannot set the expect validator to null");
338347
this.expectValidator = validator;
339348
return this;
340349
}
@@ -344,7 +353,7 @@ public HTTPServerConfiguration withExpectValidator(ExpectValidator validator) {
344353
*/
345354
@Override
346355
public HTTPServerConfiguration withHandler(HTTPHandler handler) {
347-
Objects.requireNonNull(handler, "You cannot set HTTPHandler to null");
356+
Objects.requireNonNull(handler, "You cannot set the handler to null");
348357
this.handler = handler;
349358
return this;
350359
}
@@ -354,7 +363,7 @@ public HTTPServerConfiguration withHandler(HTTPHandler handler) {
354363
*/
355364
@Override
356365
public HTTPServerConfiguration withInitialReadTimeout(Duration duration) {
357-
Objects.requireNonNull(duration, "You cannot set the client timeout to null");
366+
Objects.requireNonNull(duration, "You cannot set the client read timeout duration to null");
358367
if (duration.isZero() || duration.isNegative()) {
359368
throw new IllegalArgumentException("The client timeout duration must be greater than 0");
360369
}
@@ -379,7 +388,6 @@ public HTTPServerConfiguration withInstrumenter(Instrumenter instrumenter) {
379388
@Override
380389
public HTTPServerConfiguration withKeepAliveTimeoutDuration(Duration duration) {
381390
Objects.requireNonNull(duration, "You cannot set the keep-alive timeout duration to null");
382-
383391
if (duration.isZero() || duration.isNegative()) {
384392
throw new IllegalArgumentException("The keep-alive timeout duration must be grater than 0");
385393
}
@@ -393,7 +401,7 @@ public HTTPServerConfiguration withKeepAliveTimeoutDuration(Duration duration) {
393401
*/
394402
@Override
395403
public HTTPServerConfiguration withListener(HTTPListenerConfiguration listener) {
396-
Objects.requireNonNull(listener, "You cannot set HTTPListenerConfiguration to null");
404+
Objects.requireNonNull(listener, "You cannot add a null HTTPListenerConfiguration");
397405
this.listeners.add(listener);
398406
return this;
399407
}
@@ -403,7 +411,7 @@ public HTTPServerConfiguration withListener(HTTPListenerConfiguration listener)
403411
*/
404412
@Override
405413
public HTTPServerConfiguration withLoggerFactory(LoggerFactory loggerFactory) {
406-
Objects.requireNonNull(loggerFactory, "You cannot set LoggerFactory to null");
414+
Objects.requireNonNull(loggerFactory, "You cannot set the logger factory to null");
407415
this.loggerFactory = loggerFactory;
408416
return this;
409417
}
@@ -497,7 +505,6 @@ public HTTPServerConfiguration withMultipartBufferSize(int multipartBufferSize)
497505
@Override
498506
public HTTPServerConfiguration withMultipartConfiguration(MultipartConfiguration multipartStreamConfiguration) {
499507
Objects.requireNonNull(multipartStreamConfiguration, "You cannot set the multipart stream configuration to null");
500-
501508
this.multipartStreamConfiguration = multipartStreamConfiguration;
502509
return this;
503510
}
@@ -508,7 +515,6 @@ public HTTPServerConfiguration withMultipartConfiguration(MultipartConfiguration
508515
@Override
509516
public HTTPServerConfiguration withProcessingTimeoutDuration(Duration duration) {
510517
Objects.requireNonNull(duration, "You cannot set the processing timeout duration to null");
511-
512518
if (duration.isZero() || duration.isNegative()) {
513519
throw new IllegalArgumentException("The processing timeout duration must be grater than 0");
514520
}
@@ -523,7 +529,6 @@ public HTTPServerConfiguration withProcessingTimeoutDuration(Duration duration)
523529
@Override
524530
public HTTPServerConfiguration withReadThroughputCalculationDelayDuration(Duration duration) {
525531
Objects.requireNonNull(duration, "You cannot set the read throughput delay duration to null");
526-
527532
if (duration.isZero() || duration.isNegative()) {
528533
throw new IllegalArgumentException("The read throughput delay duration must be grater than 0");
529534
}
@@ -564,7 +569,6 @@ public HTTPServerConfiguration withResponseBufferSize(int responseBufferSize) {
564569
@Override
565570
public HTTPServerConfiguration withShutdownDuration(Duration duration) {
566571
Objects.requireNonNull(duration, "You cannot set the shutdown duration to null");
567-
568572
if (duration.isZero() || duration.isNegative()) {
569573
throw new IllegalArgumentException("The shutdown duration must be grater than 0");
570574
}
@@ -573,13 +577,22 @@ public HTTPServerConfiguration withShutdownDuration(Duration duration) {
573577
return this;
574578
}
575579

580+
/**
581+
* {@inheritDoc}
582+
*/
583+
@Override
584+
public HTTPServerConfiguration withUnexpectedExceptionHandler(HTTPUnexpectedExceptionHandler unexpectedExceptionHandler) {
585+
Objects.requireNonNull(unexpectedExceptionHandler, "You cannot set the unexpected exception handler to null");
586+
this.unexpectedExceptionHandler = unexpectedExceptionHandler;
587+
return this;
588+
}
589+
576590
/**
577591
* {@inheritDoc}
578592
*/
579593
@Override
580594
public HTTPServerConfiguration withWriteThroughputCalculationDelayDuration(Duration duration) {
581595
Objects.requireNonNull(duration, "You cannot set the write throughput delay duration to null");
582-
583596
if (duration.isZero() || duration.isNegative()) {
584597
throw new IllegalArgumentException("The write throughput delay duration must be grater than 0");
585598
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (c) 2025, FusionAuth, All Rights Reserved
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing,
11+
* software distributed under the License is distributed on an
12+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13+
* either express or implied. See the License for the specific
14+
* language governing permissions and limitations under the License.
15+
*/
16+
package io.fusionauth.http.server;
17+
18+
/**
19+
* An interface defining the HTTP unexpected exception handler contract.
20+
*
21+
* @author Daniel DeGroff
22+
*/
23+
public interface HTTPUnexpectedExceptionHandler {
24+
/**
25+
*
26+
* This handler will be called when an unexpected exception is taken while processing an HTTP request by the HTTP worker.
27+
* <p>
28+
* The intent is that this provides additional flexibility on the status code and the logging behavior when an unexpected exception
29+
* caught.
30+
*
31+
* @param context the exception context
32+
*/
33+
void handle(ExceptionHandlerContext context);
34+
}

src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import io.fusionauth.http.io.MultipartConfiguration;
3232
import io.fusionauth.http.io.PushbackInputStream;
3333
import io.fusionauth.http.log.Logger;
34+
import io.fusionauth.http.server.ExceptionHandlerContext;
3435
import io.fusionauth.http.server.HTTPHandler;
3536
import io.fusionauth.http.server.HTTPListenerConfiguration;
3637
import io.fusionauth.http.server.HTTPRequest;
@@ -104,6 +105,7 @@ public long getStartInstant() {
104105
@Override
105106
public void run() {
106107
HTTPInputStream httpInputStream;
108+
HTTPRequest request = null;
107109
HTTPResponse response = null;
108110

109111
try {
@@ -113,7 +115,7 @@ public void run() {
113115

114116
while (true) {
115117
logger.trace("[{}] Running HTTP worker. Block while we wait to read the preamble", Thread.currentThread().threadId());
116-
var request = new HTTPRequest(configuration.getContextPath(), listener.getCertificate() != null ? "https" : "http", listener.getPort(), socket.getInetAddress().getHostAddress());
118+
request = new HTTPRequest(configuration.getContextPath(), listener.getCertificate() != null ? "https" : "http", listener.getPort(), socket.getInetAddress().getHostAddress());
117119

118120
// Create a deep copy of the MultipartConfiguration so that the request may optionally modify the configuration on a per-request basis.
119121
request.getMultiPartStreamProcessor().setMultipartConfiguration(new MultipartConfiguration(configuration.getMultipartConfiguration()));
@@ -268,10 +270,14 @@ public void run() {
268270
logger.debug(String.format("[%s] Closing socket with status [%d]. An IO exception was thrown during processing. These are pretty common.", Thread.currentThread().threadId(), Status.InternalServerError), e);
269271
closeSocketOnError(response, Status.InternalServerError);
270272
} catch (Throwable e) {
271-
// Log the error and signal a failure
272-
var status = Status.InternalServerError;
273-
logger.error(String.format("[%s] Closing socket with status [%d]. An HTTP worker threw an exception while processing a request.", Thread.currentThread().threadId(), status), e);
274-
closeSocketOnError(response, status);
273+
ExceptionHandlerContext context = new ExceptionHandlerContext(logger, request, Status.InternalServerError, e);
274+
try {
275+
configuration.getUnexpectedExceptionHandler().handle(context);
276+
} catch (Throwable ignore) {
277+
}
278+
279+
// Signal an error
280+
closeSocketOnError(response, context.getStatusCode());
275281
} finally {
276282
if (instrumenter != null) {
277283
instrumenter.workerStopped();

src/test/java/io/fusionauth/http/BaseTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import io.fusionauth.http.log.Level;
5959
import io.fusionauth.http.log.LoggerFactory;
6060
import io.fusionauth.http.security.SecurityTools;
61+
import io.fusionauth.http.server.AlwaysContinueExpectValidator;
6162
import io.fusionauth.http.server.ExpectValidator;
6263
import io.fusionauth.http.server.HTTPHandler;
6364
import io.fusionauth.http.server.HTTPListenerConfiguration;
@@ -297,7 +298,7 @@ public HTTPServer makeServer(String scheme, HTTPHandler handler, Instrumenter in
297298
.withKeepAliveTimeoutDuration(ServerTimeout)
298299
.withInitialReadTimeout(ServerTimeout)
299300
.withProcessingTimeoutDuration(ServerTimeout)
300-
.withExpectValidator(expectValidator)
301+
.withExpectValidator(expectValidator != null ? expectValidator : new AlwaysContinueExpectValidator())
301302
.withInstrumenter(instrumenter)
302303
.withLoggerFactory(factory)
303304
.withMinimumReadThroughput(200 * 1024)

0 commit comments

Comments
 (0)