Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 77b3b82

Browse files
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-
varrequest = 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 : newAlwaysContinueExpectValidator())
301302
.withInstrumenter(instrumenter)
302303
.withLoggerFactory(factory)
303304
.withMinimumReadThroughput(200 * 1024)

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /