Skip to content

Commit ff4571f

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

File tree

4 files changed

+314
-4
lines changed

4 files changed

+314
-4
lines changed

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

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2016, 2024 Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2016, 2025 Oracle and/or its affiliates. All rights reserved.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -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;
@@ -146,6 +146,7 @@ protected void notifyResponse() {
146146
restrictRedirectRequest(newReq, cr);
147147

148148
final NettyConnector newConnector = new NettyConnector(newReq.getClient());
149+
ctx.close();
149150
newConnector.execute(newReq, redirectUriHistory, new CompletableFuture<ClientResponse>() {
150151
@Override
151152
public boolean complete(ClientResponse value) {
@@ -222,7 +223,7 @@ public String getReasonPhrase() {
222223

223224
if (msg instanceof LastHttpContent) {
224225
responseDone.complete(null);
225-
notifyResponse();
226+
notifyResponse(ctx);
226227
}
227228
}
228229
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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+
public 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+
}
66+
67+
public static void stop() {
68+
server.stop(0);
69+
}
70+
71+
72+
// Handler for /submit endpoint that redirects to /upload
73+
static class SubmitHandler implements HttpHandler {
74+
@Override
75+
public void handle(HttpExchange exchange) throws IOException {
76+
try {
77+
if (!"POST".equals(exchange.getRequestMethod())) {
78+
sendResponse(exchange, 405, "Method Not Allowed. Only POST is supported.");
79+
return;
80+
}
81+
82+
// Send a 307 Temporary Redirect to /upload
83+
// This preserves the POST method and body in the redirect
84+
exchange.getResponseHeaders().add("Location", "/upload");
85+
exchange.sendResponseHeaders(307, -1);
86+
} finally {
87+
exchange.close();
88+
}
89+
}
90+
}
91+
92+
// Handler for /upload endpoint that processes file uploads
93+
static class UploadHandler implements HttpHandler {
94+
@Override
95+
public void handle(HttpExchange exchange) throws IOException {
96+
try {
97+
if (!"POST".equals(exchange.getRequestMethod())) {
98+
sendResponse(exchange, 405, "Method Not Allowed. Only POST is supported.");
99+
return;
100+
}
101+
102+
// Check if the request contains multipart form data
103+
String contentType = exchange.getRequestHeaders().getFirst("Content-Type");
104+
if (contentType == null || !contentType.startsWith("multipart/form-data")) {
105+
sendResponse(exchange, 400, "Bad Request. Content type must be multipart/form-data.");
106+
return;
107+
}
108+
109+
// Extract boundary from content type
110+
String boundary = extractBoundary(contentType);
111+
if (boundary == null) {
112+
sendResponse(exchange, 400, "Bad Request. Could not determine boundary.");
113+
return;
114+
}
115+
116+
// Process the multipart request and save the file
117+
String fileName = processMultipartRequest(exchange, boundary);
118+
119+
if (fileName != null) {
120+
sendResponse(exchange, 200, "File uploaded successfully: " + fileName);
121+
} else {
122+
sendResponse(exchange, 400, "Bad Request. No file found in request.");
123+
}
124+
} catch (Exception e) {
125+
e.printStackTrace();
126+
sendResponse(exchange, 500, "Internal Server Error: " + e.getMessage());
127+
} finally {
128+
exchange.close();
129+
Files.deleteIfExists(uploadDir);
130+
}
131+
}
132+
133+
private String extractBoundary(String contentType) {
134+
int boundaryIndex = contentType.indexOf(BOUNDARY_PREFIX);
135+
if (boundaryIndex != -1) {
136+
return "--" + contentType.substring(boundaryIndex + BOUNDARY_PREFIX.length());
137+
}
138+
return null;
139+
}
140+
141+
private String processMultipartRequest(HttpExchange exchange, String boundary) throws IOException {
142+
InputStream requestBody = exchange.getRequestBody();
143+
BufferedReader reader = new BufferedReader(new InputStreamReader(requestBody, StandardCharsets.UTF_8));
144+
145+
String line;
146+
String fileName = null;
147+
Path tempFile = null;
148+
boolean isFileContent = false;
149+
150+
// Generate a random filename for the temporary file
151+
String tempFileName = UUID.randomUUID().toString();
152+
tempFile = Files.createTempFile(tempFileName, ".tmp");
153+
154+
try (OutputStream fileOut = Files.newOutputStream(tempFile)) {
155+
while ((line = reader.readLine()) != null) {
156+
// Check for the boundary
157+
if (line.startsWith(boundary)) {
158+
if (isFileContent) {
159+
// We've reached the end of the file content
160+
break;
161+
}
162+
163+
// Read the next line (Content-Disposition)
164+
line = reader.readLine();
165+
if (line != null && line.startsWith("Content-Type")) {
166+
line = reader.readLine();
167+
}
168+
if (line != null && line.contains("filename=")) {
169+
// Extract filename
170+
int filenameStart = line.indexOf("filename=\"") + 10;
171+
int filenameEnd = line.indexOf("\"", filenameStart);
172+
fileName = line.substring(filenameStart, filenameEnd);
173+
174+
// Skip Content-Type line and empty line
175+
reader.readLine(); // Content-Type
176+
// System.out.println(reader.readLine()); // Empty line
177+
isFileContent = true;
178+
}
179+
} else if (isFileContent) {
180+
// If we're reading file content and this line is not a boundary,
181+
// write it to the file (append a newline unless it's the first line)
182+
fileOut.write(line.getBytes(StandardCharsets.UTF_8));
183+
fileOut.write('\n');
184+
}
185+
}
186+
}
187+
188+
// If we found a file, move it from the temp location to the uploads directory
189+
if (fileName != null && !fileName.isEmpty()) {
190+
Path targetPath = Paths.get(UPLOAD_DIRECTORY, fileName);
191+
Files.move(tempFile, targetPath, StandardCopyOption.REPLACE_EXISTING);
192+
return fileName;
193+
} else {
194+
// If no file was found, delete the temp file
195+
Files.deleteIfExists(tempFile);
196+
return null;
197+
}
198+
}
199+
}
200+
201+
// Helper method to send HTTP responses
202+
private static void sendResponse(HttpExchange exchange, int statusCode, String response) throws IOException {
203+
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=UTF-8");
204+
exchange.sendResponseHeaders(statusCode, response.length());
205+
try (OutputStream os = exchange.getResponseBody()) {
206+
os.write(response.getBytes(StandardCharsets.UTF_8));
207+
}
208+
}
209+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
18+
package org.glassfish.jersey.tests.e2e.client;
19+
20+
import jakarta.ws.rs.client.Client;
21+
import jakarta.ws.rs.client.Entity;
22+
import jakarta.ws.rs.core.Application;
23+
import jakarta.ws.rs.core.EntityPart;
24+
import jakarta.ws.rs.core.MediaType;
25+
import jakarta.ws.rs.core.Response;
26+
import org.glassfish.jersey.client.ClientConfig;
27+
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
28+
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
29+
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
30+
import org.glassfish.jersey.netty.connector.NettyConnectorProvider;
31+
import org.glassfish.jersey.server.ResourceConfig;
32+
import org.glassfish.jersey.test.JerseyTest;
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.ByteArrayInputStream;
39+
import java.io.InputStream;
40+
import java.util.List;
41+
42+
public class RedirectLargeFileTest extends JerseyTest {
43+
44+
private final static int SERVER_PORT = 9997;
45+
private final static String SERVER_ADDR = String.format("http://localhost:%d/submit", SERVER_PORT);
46+
47+
@Override
48+
protected Application configure() {
49+
return new ResourceConfig();
50+
}
51+
52+
@Override
53+
protected void configureClient(ClientConfig config) {
54+
config.connectorProvider(new NettyConnectorProvider());
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.txt";
71+
final String path = "/bigFile/" + fileName;
72+
73+
try (final Client client = client()) {
74+
final byte[] content;
75+
try (InputStream in = RedirectLargeFileTest.class.getResourceAsStream(path)) {
76+
Assertions.assertNotNull(in, "Could not find " + path);
77+
content = in.readAllBytes();
78+
}
79+
80+
final EntityPart file = EntityPart.withFileName(fileName)
81+
.content(fileName, new ByteArrayInputStream(content))
82+
// .header("Content-Length", String.valueOf(content.length))
83+
// .mediaType(MediaType.APPLICATION_OCTET_STREAM_TYPE)
84+
.build();
85+
86+
87+
final FormDataMultiPart mp = new FormDataMultiPart();
88+
mp.bodyPart(new FormDataBodyPart(FormDataContentDisposition.name(fileName).fileName(fileName).build(),
89+
content,
90+
MediaType.TEXT_PLAIN_TYPE));
91+
92+
try (final Response response = client.target(SERVER_ADDR).request()
93+
.post(Entity.entity(mp, MediaType.MULTIPART_FORM_DATA_TYPE))) {
94+
Assertions.assertEquals(200, response.getStatus());
95+
}
96+
}
97+
}
98+
99+
}

tests/e2e-client/src/test/resources/bigFile/bigFile.txt

+1
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)