Skip to content

Commit 9de7d19

Browse files
authored
feat: add upload rejection API with optional messages (#22814)
Add ability to reject file uploads during processing with optional rejection messages. The rejection status is tracked per-file and communicated back to the client via appropriate HTTP status codes: - 200 OK: all files accepted - 422 Unprocessable Entity: all files rejected (with JSON body) - 207 Multi-Status: mixed results (with JSON body) Key changes: - Add reject() and reject(String) methods to UploadEvent - Extend UploadResult record with acceptedFiles/rejectedFiles tracking - Add UploadResult.Builder for incremental result construction - Move JSON response handling into UploadHandler.responseHandled() - Add rejected() callback to FileUploadCallback and InMemoryUploadCallback
1 parent 9aab924 commit 9de7d19

File tree

6 files changed

+575
-23
lines changed

6 files changed

+575
-23
lines changed

flow-server/src/main/java/com/vaadin/flow/server/communication/TransferUtil.java

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,15 @@
2323
import java.io.InputStream;
2424
import java.io.OutputStream;
2525
import java.io.UncheckedIOException;
26+
import java.util.ArrayList;
2627
import java.util.Collection;
2728
import java.util.Collections;
2829
import java.util.HashMap;
30+
import java.util.List;
2931
import java.util.Map;
3032
import java.util.Objects;
3133

34+
import org.slf4j.Logger;
3235
import org.slf4j.LoggerFactory;
3336

3437
import com.vaadin.flow.component.Component;
@@ -61,6 +64,10 @@ public final class TransferUtil {
6164
*/
6265
public static int DEFAULT_BUFFER_SIZE = 16384;
6366

67+
private static Logger getLogger() {
68+
return LoggerFactory.getLogger(TransferUtil.class);
69+
}
70+
6471
/**
6572
* Transfers data from the given input stream to the output stream while
6673
* notifying the progress to the given listeners.
@@ -147,6 +154,8 @@ public static void handleUpload(UploadHandler handler,
147154
VaadinRequest request, VaadinResponse response,
148155
VaadinSession session, Element owner) {
149156
boolean isMultipartUpload = isMultipartContent(request);
157+
List<String> acceptedFiles = new ArrayList<>();
158+
List<UploadResult.RejectedFile> rejectedFiles = new ArrayList<>();
150159
try {
151160
if (isMultipartUpload) {
152161
Collection<Part> parts = Collections.EMPTY_LIST;
@@ -165,9 +174,19 @@ public static void handleUpload(UploadHandler handler,
165174
session, part.getSubmittedFileName(),
166175
part.getSize(), part.getContentType(), owner,
167176
part);
177+
168178
handleUploadRequest(handler, event);
179+
180+
if (event.isRejected()) {
181+
rejectedFiles.add(new UploadResult.RejectedFile(
182+
event.getFileName(),
183+
event.getRejectionMessage()));
184+
} else {
185+
acceptedFiles.add(event.getFileName());
186+
}
169187
}
170-
handler.responseHandled(new UploadResult(true, response));
188+
handler.responseHandled(new UploadResult(true, response,
189+
null, acceptedFiles, rejectedFiles));
171190
} else {
172191
LoggerFactory.getLogger(UploadHandler.class)
173192
.warn("Multipart request has no parts");
@@ -182,7 +201,15 @@ public static void handleUpload(UploadHandler handler,
182201
owner, null);
183202

184203
handleUploadRequest(handler, event);
185-
handler.responseHandled(new UploadResult(true, response));
204+
205+
if (event.isRejected()) {
206+
rejectedFiles.add(new UploadResult.RejectedFile(
207+
event.getFileName(), event.getRejectionMessage()));
208+
} else {
209+
acceptedFiles.add(event.getFileName());
210+
}
211+
handler.responseHandled(new UploadResult(true, response, null,
212+
acceptedFiles, rejectedFiles));
186213
}
187214
} catch (UploadSizeLimitExceededException
188215
| UploadFileSizeLimitExceededException
@@ -191,19 +218,18 @@ public static void handleUpload(UploadHandler handler,
191218
+ "extend StreamRequestHandler, override {} method for "
192219
+ "UploadHandler and provide a higher limit.";
193220
if (e instanceof UploadSizeLimitExceededException) {
194-
LoggerFactory.getLogger(UploadHandler.class).warn(limitInfoStr,
195-
"Request size", "getRequestSizeMax");
221+
getLogger().warn(limitInfoStr, "Request size",
222+
"getRequestSizeMax");
196223
} else if (e instanceof UploadFileSizeLimitExceededException fileSizeException) {
197-
LoggerFactory.getLogger(UploadHandler.class).warn(
198-
limitInfoStr + " File: {}", "File size",
224+
getLogger().warn(limitInfoStr + " File: {}", "File size",
199225
"getFileSizeMax", fileSizeException.getFileName());
200226
} else if (e instanceof UploadFileCountLimitExceededException) {
201-
LoggerFactory.getLogger(UploadHandler.class).warn(limitInfoStr,
202-
"File count", "getFileCountMax");
227+
getLogger().warn(limitInfoStr, "File count", "getFileCountMax");
203228
}
204229
LoggerFactory.getLogger(UploadHandler.class)
205230
.warn("File upload failed.", e);
206-
handler.responseHandled(new UploadResult(false, response, e));
231+
handler.responseHandled(new UploadResult(false, response, e,
232+
acceptedFiles, rejectedFiles));
207233
} catch (Exception e) {
208234
if (DefaultErrorHandler.SOCKET_EXCEPTIONS
209235
.contains(e.getClass().getName())) {
@@ -214,7 +240,8 @@ public static void handleUpload(UploadHandler handler,
214240
LoggerFactory.getLogger(UploadHandler.class)
215241
.error("Exception during upload", e);
216242
}
217-
handler.responseHandled(new UploadResult(false, response, e));
243+
handler.responseHandled(new UploadResult(false, response, e,
244+
acceptedFiles, rejectedFiles));
218245
}
219246
}
220247

@@ -332,6 +359,16 @@ private static void validateUploadLimits(UploadHandler handler,
332359
}
333360
}
334361

362+
/**
363+
* Handles an upload request.
364+
*
365+
* @param handler
366+
* the upload handler
367+
* @param event
368+
* the upload event
369+
* @throws IOException
370+
* if an I/O error occurs
371+
*/
335372
private static void handleUploadRequest(UploadHandler handler,
336373
UploadEvent event) throws IOException {
337374
Component owner = event.getOwningComponent();

flow-server/src/main/java/com/vaadin/flow/server/streams/UploadEvent.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ public class UploadEvent {
5252

5353
private final Part part;
5454

55+
private boolean rejected = false;
56+
private String rejectionMessage;
57+
5558
/**
5659
* Create a new download event with required data.
5760
*
@@ -90,8 +93,15 @@ public UploadEvent(VaadinRequest request, VaadinResponse response,
9093
*
9194
* @return the input stream from which the contents of the request can be
9295
* read
96+
* @throws IllegalStateException
97+
* if the upload has been rejected
9398
*/
9499
public InputStream getInputStream() {
100+
if (rejected) {
101+
throw new IllegalStateException(
102+
"Cannot access input stream of rejected upload: "
103+
+ rejectionMessage);
104+
}
95105
try {
96106
if (part != null) {
97107
return part.getInputStream();
@@ -201,4 +211,51 @@ private UI getUiFromSession(Component value) {
201211
session.unlock();
202212
}
203213
}
214+
215+
/**
216+
* Rejects this upload with a default message.
217+
* <p>
218+
* When called, the file will not be processed (or will be cleaned up if
219+
* already processed) and the rejection will be communicated to the client.
220+
* The default rejection message "File rejected" will be used.
221+
*
222+
* @see #reject(String)
223+
*/
224+
public void reject() {
225+
reject("File rejected");
226+
}
227+
228+
/**
229+
* Rejects this upload with a custom message.
230+
* <p>
231+
* When called, the file will not be processed (or will be cleaned up if
232+
* already processed) and the rejection will be communicated to the client
233+
* with the provided message.
234+
*
235+
* @param message
236+
* the rejection message to send to the client
237+
*/
238+
public void reject(String message) {
239+
this.rejected = true;
240+
this.rejectionMessage = message;
241+
}
242+
243+
/**
244+
* Checks whether this upload has been rejected.
245+
*
246+
* @return {@code true} if the upload has been rejected, {@code false}
247+
* otherwise
248+
*/
249+
public boolean isRejected() {
250+
return rejected;
251+
}
252+
253+
/**
254+
* Gets the rejection message if this upload has been rejected.
255+
*
256+
* @return the rejection message, or {@code null} if not rejected
257+
*/
258+
public String getRejectionMessage() {
259+
return rejectionMessage;
260+
}
204261
}

flow-server/src/main/java/com/vaadin/flow/server/streams/UploadHandler.java

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@
1616
package com.vaadin.flow.server.streams;
1717

1818
import java.io.IOException;
19+
import java.io.PrintWriter;
20+
import java.util.List;
21+
22+
import org.slf4j.LoggerFactory;
23+
import tools.jackson.core.JacksonException;
24+
import tools.jackson.databind.ObjectMapper;
1925

2026
import com.vaadin.flow.dom.Element;
27+
import com.vaadin.flow.internal.JacksonUtils;
2128
import com.vaadin.flow.server.HttpStatusCode;
2229
import com.vaadin.flow.server.VaadinRequest;
2330
import com.vaadin.flow.server.VaadinResponse;
@@ -111,25 +118,79 @@ public interface UploadHandler extends ElementRequestHandler {
111118
* {@link UploadHandler#handleUploadRequest(UploadEvent)} methods have been
112119
* called for all files.
113120
* <p>
114-
* This method sets the http response return codes according to internal
115-
* exception handling in the framework.
121+
* This method sets the HTTP response return codes and writes JSON responses
122+
* for rejected files:
123+
* <ul>
124+
* <li>200 OK - all files accepted</li>
125+
* <li>422 Unprocessable Entity - all files rejected (with JSON body)</li>
126+
* <li>207 Multi-Status - some files accepted, some rejected (with JSON
127+
* body)</li>
128+
* <li>500 Internal Server Error - exception occurred</li>
129+
* </ul>
116130
* <p>
117131
* If you want custom exception handling and to set the return code,
118132
* implement this method and overwrite the default functionality.
119133
*
120134
* @param result
121135
* the result of the upload operation containing success status,
122-
* response object, and any exception that occurred
136+
* response object, any exception that occurred, and lists of
137+
* accepted/rejected files
123138
*/
124139
default void responseHandled(UploadResult result) {
125-
if (result.success()) {
126-
result.response().setStatus(HttpStatusCode.OK.getCode());
127-
} else {
128-
result.response()
129-
.setStatus(HttpStatusCode.INTERNAL_SERVER_ERROR.getCode());
140+
VaadinResponse response = result.response();
141+
try {
142+
if (result.exception() != null) {
143+
response.setStatus(
144+
HttpStatusCode.INTERNAL_SERVER_ERROR.getCode());
145+
} else if (result.allRejected()) {
146+
response.setStatus(422); // Unprocessable Entity
147+
response.setContentType("application/json");
148+
writeJsonResponse(response,
149+
new RejectedFilesResponse(result.rejectedFiles()));
150+
} else if (result.hasMixed()) {
151+
response.setStatus(207); // Multi-Status
152+
response.setContentType("application/json");
153+
writeJsonResponse(response, new MixedUploadResponse(
154+
result.acceptedFiles(), result.rejectedFiles()));
155+
} else {
156+
response.setStatus(HttpStatusCode.OK.getCode());
157+
}
158+
} catch (IOException e) {
159+
LoggerFactory.getLogger(UploadHandler.class)
160+
.error("Error writing upload response", e);
161+
response.setStatus(HttpStatusCode.INTERNAL_SERVER_ERROR.getCode());
162+
}
163+
}
164+
165+
private static void writeJsonResponse(VaadinResponse response,
166+
Object responseObject) throws IOException {
167+
ObjectMapper mapper = JacksonUtils.getMapper();
168+
try {
169+
String json = mapper.writeValueAsString(responseObject);
170+
PrintWriter writer = response.getWriter();
171+
writer.write(json);
172+
} catch (JacksonException e) {
173+
throw new IOException("Failed to serialize response to JSON", e);
130174
}
131175
}
132176

177+
/**
178+
* JSON response structure for rejected files.
179+
*/
180+
record RejectedFilesResponse(List<UploadResult.RejectedFile> rejected)
181+
implements
182+
java.io.Serializable {
183+
}
184+
185+
/**
186+
* JSON response structure for mixed upload results.
187+
*/
188+
record MixedUploadResponse(List<String> accepted,
189+
List<UploadResult.RejectedFile> rejected)
190+
implements
191+
java.io.Serializable {
192+
}
193+
133194
default void handleRequest(VaadinRequest request, VaadinResponse response,
134195
VaadinSession session, Element owner) throws IOException {
135196
TransferUtil.handleUpload(this, request, response, session, owner);

0 commit comments

Comments
 (0)