Skip to content

[2.x] File upload handling after redirect #5894

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class JerseyClientHandler extends SimpleChannelInboundHandler<HttpObject> {

@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
notifyResponse();
notifyResponse(ctx);
}

@Override
Expand All @@ -104,7 +104,7 @@ public void channelInactive(ChannelHandlerContext ctx) {
}
}

protected void notifyResponse() {
protected void notifyResponse(ChannelHandlerContext ctx) {
if (jerseyResponse != null) {
ClientResponse cr = jerseyResponse;
jerseyResponse = null;
Expand Down Expand Up @@ -143,6 +143,7 @@ protected void notifyResponse() {
} else {
ClientRequest newReq = new ClientRequest(jerseyRequest);
newReq.setUri(newUri);
ctx.close();
if (redirectController.prepareRedirect(newReq, cr)) {
final NettyConnector newConnector = new NettyConnector(newReq.getClient());
newConnector.execute(newReq, redirectUriHistory, new CompletableFuture<ClientResponse>() {
Expand Down Expand Up @@ -224,7 +225,7 @@ public String getReasonPhrase() {

if (msg instanceof LastHttpContent) {
responseDone.complete(null);
notifyResponse();
notifyResponse(ctx);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/*
* Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/

package org.glassfish.jersey.tests.e2e.client;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.UUID;
import java.util.concurrent.Executors;


/**
* Server for the file upload test that redirects from /submit to /upload.
*/
class RedirectFileUploadServerTest {
private static final String UPLOAD_DIRECTORY = "target/uploads";
private static final String BOUNDARY_PREFIX = "boundary=";
private static final Path uploadDir = Paths.get(UPLOAD_DIRECTORY);

private static HttpServer server;


static void start(int port) throws IOException {
// Create upload directory if it doesn't exist
if (!Files.exists(uploadDir)) {
Files.createDirectory(uploadDir);
}

// Create HTTP server
server = HttpServer.create(new InetSocketAddress(port), 0);

// Create contexts for different endpoints
server.createContext("/submit", new SubmitHandler());
server.createContext("/upload", new UploadHandler());

// Set executor and start server
server.setExecutor(Executors.newFixedThreadPool(10));
server.start();
System.out.println("Server running on port " + port);
}

public static void stop() {
server.stop(0);
}


// Handler for /submit endpoint that redirects to /upload
static class SubmitHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
try {
if (!"POST".equals(exchange.getRequestMethod())) {
sendResponse(exchange, 405, "Method Not Allowed. Only POST is supported.");
return;
}

final BufferedReader reader
= new BufferedReader(new InputStreamReader(exchange.getRequestBody(), StandardCharsets.UTF_8));
while (reader.readLine() != null) {
//discard payload - required for JDK 1.8
}
reader.close();

// Send a 307 Temporary Redirect to /upload
// This preserves the POST method and body in the redirect
exchange.getResponseHeaders().add("Location", "/upload");
exchange.sendResponseHeaders(307, -1);
} finally {
exchange.close();
}
}
}

// Handler for /upload endpoint that processes file uploads
static class UploadHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
try {
if (!"POST".equals(exchange.getRequestMethod())) {
sendResponse(exchange, 405, "Method Not Allowed. Only POST is supported.");
return;
}

// Check if the request contains multipart form data
String contentType = exchange.getRequestHeaders().getFirst("Content-Type");
if (contentType == null || !contentType.startsWith("multipart/form-data")) {
sendResponse(exchange, 400, "Bad Request. Content type must be multipart/form-data.");
return;
}

// Extract boundary from content type
String boundary = extractBoundary(contentType);
if (boundary == null) {
sendResponse(exchange, 400, "Bad Request. Could not determine boundary.");
return;
}

// Process the multipart request and save the file
String fileName = processMultipartRequest(exchange, boundary);

if (fileName != null) {
sendResponse(exchange, 200, "File uploaded successfully: " + fileName);
} else {
sendResponse(exchange, 400, "Bad Request. No file found in request.");
}
} catch (Exception e) {
e.printStackTrace();
sendResponse(exchange, 500, "Internal Server Error: " + e.getMessage());
} finally {
exchange.close();
Files.deleteIfExists(uploadDir);
}
}

private String extractBoundary(String contentType) {
int boundaryIndex = contentType.indexOf(BOUNDARY_PREFIX);
if (boundaryIndex != -1) {
return "--" + contentType.substring(boundaryIndex + BOUNDARY_PREFIX.length());
}
return null;
}

private String processMultipartRequest(HttpExchange exchange, String boundary) throws IOException {
InputStream requestBody = exchange.getRequestBody();
BufferedReader reader = new BufferedReader(new InputStreamReader(requestBody, StandardCharsets.UTF_8));

String line;
String fileName = null;
Path tempFile = null;
boolean isFileContent = false;

// Generate a random filename for the temporary file
String tempFileName = UUID.randomUUID().toString();
tempFile = Files.createTempFile(tempFileName, ".tmp");

try (OutputStream fileOut = Files.newOutputStream(tempFile)) {
while ((line = reader.readLine()) != null) {
// Check for the boundary
if (line.startsWith(boundary)) {
if (isFileContent) {
// We've reached the end of the file content
break;
}

// Read the next line (Content-Disposition)
line = reader.readLine();
if (line != null && line.startsWith("Content-Type")) {
line = reader.readLine();
}
if (line != null && line.contains("filename=")) {
// Extract filename
int filenameStart = line.indexOf("filename=\"") + 10;
int filenameEnd = line.indexOf("\"", filenameStart);
fileName = line.substring(filenameStart, filenameEnd);

// Skip Content-Type line and empty line
reader.readLine(); // Content-Type
// System.out.println(reader.readLine()); // Empty line
isFileContent = true;
}
} else if (isFileContent) {
// If we're reading file content and this line is not a boundary,
// write it to the file (append a newline unless it's the first line)
fileOut.write(line.getBytes(StandardCharsets.UTF_8));
fileOut.write('\n');
}
}
}

// If we found a file, move it from the temp location to the uploads directory
if (fileName != null && !fileName.isEmpty()) {
Path targetPath = Paths.get(UPLOAD_DIRECTORY, fileName);
Files.move(tempFile, targetPath, StandardCopyOption.REPLACE_EXISTING);
return fileName;
} else {
// If no file was found, delete the temp file
Files.deleteIfExists(tempFile);
return null;
}
}
}

// Helper method to send HTTP responses
private static void sendResponse(HttpExchange exchange, int statusCode, String response) throws IOException {
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=UTF-8");
exchange.sendResponseHeaders(statusCode, response.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes(StandardCharsets.UTF_8));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/

package org.glassfish.jersey.tests.e2e.client;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.netty.connector.NettyConnectorProvider;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class RedirectLargeFileTest {

private static final int SERVER_PORT = 9997;
private static final String SERVER_ADDR = String.format("http://localhost:%d/submit", SERVER_PORT);

Client client() {
final ClientConfig config = new ClientConfig();
config.connectorProvider(new NettyConnectorProvider());
config.register(MultiPartFeature.class);
return ClientBuilder.newClient(config);
}

@BeforeAll
static void startServer() throws Exception{
RedirectFileUploadServerTest.start(SERVER_PORT);
}

@AfterAll
static void stopServer() {
RedirectFileUploadServerTest.stop();
}

@Test
void sendFileTest() throws Exception {

final String fileName = "bigFile.json";
final String path = "target/" + fileName;

final Path pathResource = Paths.get(path);
try {
final Path realFilePath = Files.createFile(pathResource.toAbsolutePath());

generateJson(realFilePath.toString(), 1000000); // 33Mb real file size

final byte[] content = Files.readAllBytes(realFilePath);

final FormDataMultiPart mp = new FormDataMultiPart();
mp.bodyPart(new FormDataBodyPart(FormDataContentDisposition.name(fileName).fileName(fileName).build(),
content,
MediaType.TEXT_PLAIN_TYPE));

try (final Response response = client().target(SERVER_ADDR).request()
.post(Entity.entity(mp, MediaType.MULTIPART_FORM_DATA_TYPE))) {
Assertions.assertEquals(200, response.getStatus());
}
} finally {
Files.deleteIfExists(pathResource);
}
}

private static void generateJson(final String filePath, int recordCount) throws Exception {

try (final JsonGenerator generator = new JsonFactory().createGenerator(new FileWriter(filePath))) {
generator.writeStartArray();

for (int i = 0; i < recordCount; i++) {
generator.writeStartObject();
generator.writeNumberField("id", i);
generator.writeStringField("name", "User" + i);
// Add more fields as needed
generator.writeEndObject();

if (i % 10000 == 0) {
generator.flush();
}
}

generator.writeEndArray();
}
}
}
Loading