Skip to content

Commit 7d7377a

Browse files
committed
File upload handling after redirect
Signed-off-by: Maxim Nesen <[email protected]>
1 parent 3357cb8 commit 7d7377a

File tree

3 files changed

+336
-3
lines changed

3 files changed

+336
-3
lines changed

connectors/netty-connector/src/main/java/org/glassfish/jersey/netty/connector/JerseyClientHandler.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ class JerseyClientHandler extends SimpleChannelInboundHandler<HttpObject> {
8888

8989
@Override
9090
public void channelReadComplete(ChannelHandlerContext ctx) {
91-
notifyResponse();
91+
notifyResponse(ctx);
9292
}
9393

9494
@Override
@@ -104,7 +104,7 @@ public void channelInactive(ChannelHandlerContext ctx) {
104104
}
105105
}
106106

107-
protected void notifyResponse() {
107+
protected void notifyResponse(ChannelHandlerContext ctx) {
108108
if (jerseyResponse != null) {
109109
ClientResponse cr = jerseyResponse;
110110
jerseyResponse = null;
@@ -143,6 +143,7 @@ protected void notifyResponse() {
143143
} else {
144144
ClientRequest newReq = new ClientRequest(jerseyRequest);
145145
newReq.setUri(newUri);
146+
ctx.close();
146147
if (redirectController.prepareRedirect(newReq, cr)) {
147148
final NettyConnector newConnector = new NettyConnector(newReq.getClient());
148149
newConnector.execute(newReq, redirectUriHistory, new CompletableFuture<ClientResponse>() {
@@ -224,7 +225,7 @@ public String getReasonPhrase() {
224225

225226
if (msg instanceof LastHttpContent) {
226227
responseDone.complete(null);
227-
notifyResponse();
228+
notifyResponse(ctx);
228229
}
229230
}
230231
}
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/*
2+
* Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0, which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the
10+
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
11+
* version 2 with the GNU Classpath Exception, which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
*/
16+
17+
package org.glassfish.jersey.tests.e2e.client;
18+
19+
import com.sun.net.httpserver.HttpExchange;
20+
import com.sun.net.httpserver.HttpHandler;
21+
import com.sun.net.httpserver.HttpServer;
22+
23+
import java.io.BufferedReader;
24+
import java.io.IOException;
25+
import java.io.InputStream;
26+
import java.io.InputStreamReader;
27+
import java.io.OutputStream;
28+
import java.net.InetSocketAddress;
29+
import java.nio.charset.StandardCharsets;
30+
import java.nio.file.Files;
31+
import java.nio.file.Path;
32+
import java.nio.file.Paths;
33+
import java.nio.file.StandardCopyOption;
34+
import java.util.UUID;
35+
import java.util.concurrent.Executors;
36+
37+
38+
/**
39+
* Server for the file upload test that redirects from /submit to /upload.
40+
*/
41+
class RedirectFileUploadServerTest {
42+
private static final String UPLOAD_DIRECTORY = "target/uploads";
43+
private static final String BOUNDARY_PREFIX = "boundary=";
44+
private static final Path uploadDir = Paths.get(UPLOAD_DIRECTORY);
45+
46+
private static HttpServer server;
47+
48+
49+
static void start(int port) throws IOException {
50+
// Create upload directory if it doesn't exist
51+
if (!Files.exists(uploadDir)) {
52+
Files.createDirectory(uploadDir);
53+
}
54+
55+
// Create HTTP server
56+
server = HttpServer.create(new InetSocketAddress(port), 0);
57+
58+
// Create contexts for different endpoints
59+
server.createContext("/submit", new SubmitHandler());
60+
server.createContext("/upload", new UploadHandler());
61+
62+
// Set executor and start server
63+
server.setExecutor(Executors.newFixedThreadPool(10));
64+
server.start();
65+
System.out.println("Server running on port " + port);
66+
}
67+
68+
public static void stop() {
69+
server.stop(0);
70+
}
71+
72+
73+
// Handler for /submit endpoint that redirects to /upload
74+
static class SubmitHandler implements HttpHandler {
75+
@Override
76+
public void handle(HttpExchange exchange) throws IOException {
77+
try {
78+
if (!"POST".equals(exchange.getRequestMethod())) {
79+
sendResponse(exchange, 405, "Method Not Allowed. Only POST is supported.");
80+
return;
81+
}
82+
83+
final BufferedReader reader
84+
= new BufferedReader(new InputStreamReader(exchange.getRequestBody(), StandardCharsets.UTF_8));
85+
while (reader.readLine() != null) {
86+
//discard payload - required for JDK 1.8
87+
}
88+
reader.close();
89+
90+
// Send a 307 Temporary Redirect to /upload
91+
// This preserves the POST method and body in the redirect
92+
exchange.getResponseHeaders().add("Location", "/upload");
93+
exchange.sendResponseHeaders(307, -1);
94+
} finally {
95+
exchange.close();
96+
}
97+
}
98+
}
99+
100+
// Handler for /upload endpoint that processes file uploads
101+
static class UploadHandler implements HttpHandler {
102+
@Override
103+
public void handle(HttpExchange exchange) throws IOException {
104+
try {
105+
if (!"POST".equals(exchange.getRequestMethod())) {
106+
sendResponse(exchange, 405, "Method Not Allowed. Only POST is supported.");
107+
return;
108+
}
109+
110+
// Check if the request contains multipart form data
111+
String contentType = exchange.getRequestHeaders().getFirst("Content-Type");
112+
if (contentType == null || !contentType.startsWith("multipart/form-data")) {
113+
sendResponse(exchange, 400, "Bad Request. Content type must be multipart/form-data.");
114+
return;
115+
}
116+
117+
// Extract boundary from content type
118+
String boundary = extractBoundary(contentType);
119+
if (boundary == null) {
120+
sendResponse(exchange, 400, "Bad Request. Could not determine boundary.");
121+
return;
122+
}
123+
124+
// Process the multipart request and save the file
125+
String fileName = processMultipartRequest(exchange, boundary);
126+
127+
if (fileName != null) {
128+
sendResponse(exchange, 200, "File uploaded successfully: " + fileName);
129+
} else {
130+
sendResponse(exchange, 400, "Bad Request. No file found in request.");
131+
}
132+
} catch (Exception e) {
133+
e.printStackTrace();
134+
sendResponse(exchange, 500, "Internal Server Error: " + e.getMessage());
135+
} finally {
136+
exchange.close();
137+
Files.deleteIfExists(uploadDir);
138+
}
139+
}
140+
141+
private String extractBoundary(String contentType) {
142+
int boundaryIndex = contentType.indexOf(BOUNDARY_PREFIX);
143+
if (boundaryIndex != -1) {
144+
return "--" + contentType.substring(boundaryIndex + BOUNDARY_PREFIX.length());
145+
}
146+
return null;
147+
}
148+
149+
private String processMultipartRequest(HttpExchange exchange, String boundary) throws IOException {
150+
InputStream requestBody = exchange.getRequestBody();
151+
BufferedReader reader = new BufferedReader(new InputStreamReader(requestBody, StandardCharsets.UTF_8));
152+
153+
String line;
154+
String fileName = null;
155+
Path tempFile = null;
156+
boolean isFileContent = false;
157+
158+
// Generate a random filename for the temporary file
159+
String tempFileName = UUID.randomUUID().toString();
160+
tempFile = Files.createTempFile(tempFileName, ".tmp");
161+
162+
try (OutputStream fileOut = Files.newOutputStream(tempFile)) {
163+
while ((line = reader.readLine()) != null) {
164+
// Check for the boundary
165+
if (line.startsWith(boundary)) {
166+
if (isFileContent) {
167+
// We've reached the end of the file content
168+
break;
169+
}
170+
171+
// Read the next line (Content-Disposition)
172+
line = reader.readLine();
173+
if (line != null && line.startsWith("Content-Type")) {
174+
line = reader.readLine();
175+
}
176+
if (line != null && line.contains("filename=")) {
177+
// Extract filename
178+
int filenameStart = line.indexOf("filename=\"") + 10;
179+
int filenameEnd = line.indexOf("\"", filenameStart);
180+
fileName = line.substring(filenameStart, filenameEnd);
181+
182+
// Skip Content-Type line and empty line
183+
reader.readLine(); // Content-Type
184+
// System.out.println(reader.readLine()); // Empty line
185+
isFileContent = true;
186+
}
187+
} else if (isFileContent) {
188+
// If we're reading file content and this line is not a boundary,
189+
// write it to the file (append a newline unless it's the first line)
190+
fileOut.write(line.getBytes(StandardCharsets.UTF_8));
191+
fileOut.write('\n');
192+
}
193+
}
194+
}
195+
196+
// If we found a file, move it from the temp location to the uploads directory
197+
if (fileName != null && !fileName.isEmpty()) {
198+
Path targetPath = Paths.get(UPLOAD_DIRECTORY, fileName);
199+
Files.move(tempFile, targetPath, StandardCopyOption.REPLACE_EXISTING);
200+
return fileName;
201+
} else {
202+
// If no file was found, delete the temp file
203+
Files.deleteIfExists(tempFile);
204+
return null;
205+
}
206+
}
207+
}
208+
209+
// Helper method to send HTTP responses
210+
private static void sendResponse(HttpExchange exchange, int statusCode, String response) throws IOException {
211+
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=UTF-8");
212+
exchange.sendResponseHeaders(statusCode, response.length());
213+
try (OutputStream os = exchange.getResponseBody()) {
214+
os.write(response.getBytes(StandardCharsets.UTF_8));
215+
}
216+
}
217+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0, which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the
10+
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
11+
* version 2 with the GNU Classpath Exception, which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
*/
16+
17+
package org.glassfish.jersey.tests.e2e.client;
18+
19+
import javax.ws.rs.client.Client;
20+
import javax.ws.rs.client.ClientBuilder;
21+
import javax.ws.rs.client.Entity;
22+
import javax.ws.rs.core.MediaType;
23+
import javax.ws.rs.core.Response;
24+
25+
import com.fasterxml.jackson.core.JsonFactory;
26+
import com.fasterxml.jackson.core.JsonGenerator;
27+
import org.glassfish.jersey.client.ClientConfig;
28+
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
29+
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
30+
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
31+
import org.glassfish.jersey.media.multipart.MultiPartFeature;
32+
import org.glassfish.jersey.netty.connector.NettyConnectorProvider;
33+
import org.junit.jupiter.api.AfterAll;
34+
import org.junit.jupiter.api.Assertions;
35+
import org.junit.jupiter.api.BeforeAll;
36+
import org.junit.jupiter.api.Test;
37+
38+
import java.io.FileWriter;
39+
import java.io.IOException;
40+
import java.net.URL;
41+
import java.nio.file.Files;
42+
import java.nio.file.Path;
43+
import java.nio.file.Paths;
44+
45+
public class RedirectLargeFileTest {
46+
47+
private static final int SERVER_PORT = 9997;
48+
private static final String SERVER_ADDR = String.format("http://localhost:%d/submit", SERVER_PORT);
49+
50+
Client client() {
51+
final ClientConfig config = new ClientConfig();
52+
config.connectorProvider(new NettyConnectorProvider());
53+
config.register(MultiPartFeature.class);
54+
return ClientBuilder.newClient(config);
55+
}
56+
57+
@BeforeAll
58+
static void startServer() throws Exception{
59+
RedirectFileUploadServerTest.start(SERVER_PORT);
60+
}
61+
62+
@AfterAll
63+
static void stopServer() {
64+
RedirectFileUploadServerTest.stop();
65+
}
66+
67+
@Test
68+
void sendFileTest() throws Exception {
69+
70+
final String fileName = "bigFile.json";
71+
final String path = "target/" + fileName;
72+
73+
final Path pathResource = Paths.get(path);
74+
try {
75+
final Path realFilePath = Files.createFile(pathResource.toAbsolutePath());
76+
77+
generateJson(realFilePath.toString(), 1000000); // 33Mb real file size
78+
79+
final byte[] content = Files.readAllBytes(realFilePath);
80+
81+
final FormDataMultiPart mp = new FormDataMultiPart();
82+
mp.bodyPart(new FormDataBodyPart(FormDataContentDisposition.name(fileName).fileName(fileName).build(),
83+
content,
84+
MediaType.TEXT_PLAIN_TYPE));
85+
86+
try (final Response response = client().target(SERVER_ADDR).request()
87+
.post(Entity.entity(mp, MediaType.MULTIPART_FORM_DATA_TYPE))) {
88+
Assertions.assertEquals(200, response.getStatus());
89+
}
90+
} finally {
91+
Files.deleteIfExists(pathResource);
92+
}
93+
}
94+
95+
private static void generateJson(final String filePath, int recordCount) throws Exception {
96+
97+
try (final JsonGenerator generator = new JsonFactory().createGenerator(new FileWriter(filePath))) {
98+
generator.writeStartArray();
99+
100+
for (int i = 0; i < recordCount; i++) {
101+
generator.writeStartObject();
102+
generator.writeNumberField("id", i);
103+
generator.writeStringField("name", "User" + i);
104+
// Add more fields as needed
105+
generator.writeEndObject();
106+
107+
if (i % 10000 == 0) {
108+
generator.flush();
109+
}
110+
}
111+
112+
generator.writeEndArray();
113+
}
114+
}
115+
}

0 commit comments

Comments
 (0)