Skip to content

Commit eff2c54

Browse files
committed
feat: add upload rejection API with optional messages
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 81d9fc4 commit eff2c54

File tree

8 files changed

+748
-26
lines changed

8 files changed

+748
-26
lines changed

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

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.util.Map;
3030
import java.util.Objects;
3131

32+
import org.slf4j.Logger;
3233
import org.slf4j.LoggerFactory;
3334

3435
import com.vaadin.flow.component.Component;
@@ -52,6 +53,9 @@
5253
*/
5354
public final class TransferUtil {
5455

56+
private static final Logger logger = LoggerFactory
57+
.getLogger(TransferUtil.class);
58+
5559
/**
5660
* Default buffer size for reading data from the input stream.
5761
* <p>
@@ -60,6 +64,10 @@ public final class TransferUtil {
6064
*/
6165
public static int DEFAULT_BUFFER_SIZE = 16384;
6266

67+
private static Logger getLogger() {
68+
return logger;
69+
}
70+
6371
/**
6472
* Transfers data from the given input stream to the output stream while
6573
* notifying the progress to the given listeners.
@@ -146,6 +154,7 @@ public static void handleUpload(UploadHandler handler,
146154
VaadinRequest request, VaadinResponse response,
147155
VaadinSession session, Element owner) {
148156
boolean isMultipartUpload = isMultipartContent(request);
157+
UploadResult.Builder resultBuilder = new UploadResult.Builder(response);
149158
try {
150159
if (isMultipartUpload) {
151160
Collection<Part> parts = Collections.EMPTY_LIST;
@@ -164,9 +173,10 @@ public static void handleUpload(UploadHandler handler,
164173
session, part.getSubmittedFileName(),
165174
part.getSize(), part.getContentType(), owner,
166175
part);
167-
handleUploadRequest(handler, event);
176+
177+
handleUploadRequest(handler, event, resultBuilder);
168178
}
169-
handler.responseHandled(new UploadResult(true, response));
179+
handler.responseHandled(resultBuilder.build());
170180
} else {
171181
LoggerFactory.getLogger(UploadHandler.class)
172182
.warn("Multipart request has no parts");
@@ -180,8 +190,8 @@ public static void handleUpload(UploadHandler handler,
180190
fileName, request.getContentLengthLong(), contentType,
181191
owner, null);
182192

183-
handleUploadRequest(handler, event);
184-
handler.responseHandled(new UploadResult(true, response));
193+
handleUploadRequest(handler, event, resultBuilder);
194+
handler.responseHandled(resultBuilder.build());
185195
}
186196
} catch (UploadSizeLimitExceededException
187197
| UploadFileSizeLimitExceededException
@@ -190,23 +200,24 @@ public static void handleUpload(UploadHandler handler,
190200
+ "extend StreamRequestHandler, override {} method for "
191201
+ "UploadHandler and provide a higher limit.";
192202
if (e instanceof UploadSizeLimitExceededException) {
193-
LoggerFactory.getLogger(UploadHandler.class).warn(limitInfoStr,
194-
"Request size", "getRequestSizeMax");
203+
getLogger().warn(limitInfoStr, "Request size",
204+
"getRequestSizeMax");
195205
} else if (e instanceof UploadFileSizeLimitExceededException fileSizeException) {
196-
LoggerFactory.getLogger(UploadHandler.class).warn(
197-
limitInfoStr + " File: {}", "File size",
206+
getLogger().warn(limitInfoStr + " File: {}", "File size",
198207
"getFileSizeMax", fileSizeException.getFileName());
199208
} else if (e instanceof UploadFileCountLimitExceededException) {
200-
LoggerFactory.getLogger(UploadHandler.class).warn(limitInfoStr,
201-
"File count", "getFileCountMax");
209+
getLogger().warn(limitInfoStr, "File count",
210+
"getFileCountMax");
202211
}
203212
LoggerFactory.getLogger(UploadHandler.class)
204213
.warn("File upload failed.", e);
205-
handler.responseHandled(new UploadResult(false, response, e));
214+
handler.responseHandled(
215+
resultBuilder.withException(e).build());
206216
} catch (Exception e) {
207217
LoggerFactory.getLogger(UploadHandler.class)
208218
.error("Exception during upload", e);
209-
handler.responseHandled(new UploadResult(false, response, e));
219+
handler.responseHandled(
220+
resultBuilder.withException(e).build());
210221
}
211222
}
212223

@@ -324,14 +335,36 @@ private static void validateUploadLimits(UploadHandler handler,
324335
}
325336
}
326337

338+
/**
339+
* Handles an upload request and checks for rejection.
340+
*
341+
* @param handler
342+
* the upload handler
343+
* @param event
344+
* the upload event
345+
* @param resultBuilder
346+
* the upload result builder to update with acceptance/rejection
347+
* @throws IOException
348+
* if an I/O error occurs
349+
*/
327350
private static void handleUploadRequest(UploadHandler handler,
328-
UploadEvent event) throws IOException {
351+
UploadEvent event, UploadResult.Builder resultBuilder)
352+
throws IOException {
329353
Component owner = event.getOwningComponent();
330354
try {
331355
ComponentUtil.fireEvent(owner, new UploadStartEvent(owner));
332356
handler.handleUploadRequest(event);
333357
} finally {
334358
ComponentUtil.fireEvent(owner, new UploadCompleteEvent(owner));
335359
}
360+
361+
if (event.isRejected()) {
362+
getLogger().debug("File rejected: {} - {}", event.getFileName(),
363+
event.getRejectionMessage());
364+
resultBuilder.addRejected(event.getFileName(),
365+
event.getRejectionMessage());
366+
} else {
367+
resultBuilder.addAccepted(event.getFileName());
368+
}
336369
}
337370
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,27 @@ public interface FileUploadCallback extends Serializable {
4646
* if an I/O error occurs in the callback
4747
*/
4848
void complete(UploadMetadata metadata, File file) throws IOException;
49+
50+
/**
51+
* Called when a file upload is rejected.
52+
* <p>
53+
* This method is invoked when {@link UploadEvent#reject()} or
54+
* {@link UploadEvent#reject(String)} is called, allowing the handler to
55+
* perform cleanup or logging for rejected uploads.
56+
* <p>
57+
* The default implementation does nothing. Override this method to handle
58+
* rejections.
59+
*
60+
* @param metadata
61+
* the upload metadata containing relevant information about the
62+
* rejected upload
63+
* @param reason
64+
* the reason for rejection
65+
* @throws IOException
66+
* if an I/O error occurs in the callback
67+
*/
68+
default void rejected(UploadMetadata metadata, String reason)
69+
throws IOException {
70+
// Default implementation does nothing
71+
}
4972
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,27 @@ public interface InMemoryUploadCallback extends Serializable {
4242
* if an I/O error occurs in the callback
4343
*/
4444
void complete(UploadMetadata metadata, byte[] data) throws IOException;
45+
46+
/**
47+
* Called when a file upload is rejected.
48+
* <p>
49+
* This method is invoked when {@link UploadEvent#reject()} or
50+
* {@link UploadEvent#reject(String)} is called, allowing the handler to
51+
* perform cleanup or logging for rejected uploads.
52+
* <p>
53+
* The default implementation does nothing. Override this method to handle
54+
* rejections.
55+
*
56+
* @param metadata
57+
* the upload metadata containing relevant information about the
58+
* rejected upload
59+
* @param reason
60+
* the reason for rejection
61+
* @throws IOException
62+
* if an I/O error occurs in the callback
63+
*/
64+
default void rejected(UploadMetadata metadata, String reason)
65+
throws IOException {
66+
// Default implementation does nothing
67+
}
4568
}

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,16 @@
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.Logger;
23+
import org.slf4j.LoggerFactory;
24+
import tools.jackson.core.JacksonException;
25+
import tools.jackson.databind.ObjectMapper;
1926

2027
import com.vaadin.flow.dom.Element;
28+
import com.vaadin.flow.internal.JacksonUtils;
2129
import com.vaadin.flow.server.HttpStatusCode;
2230
import com.vaadin.flow.server.VaadinRequest;
2331
import com.vaadin.flow.server.VaadinResponse;
@@ -111,25 +119,78 @@ public interface UploadHandler extends ElementRequestHandler {
111119
* {@link UploadHandler#handleUploadRequest(UploadEvent)} methods have been
112120
* called for all files.
113121
* <p>
114-
* This method sets the http response return codes according to internal
115-
* exception handling in the framework.
122+
* This method sets the HTTP response return codes and writes JSON responses
123+
* for rejected files:
124+
* <ul>
125+
* <li>200 OK - all files accepted</li>
126+
* <li>422 Unprocessable Entity - all files rejected (with JSON body)</li>
127+
* <li>207 Multi-Status - some files accepted, some rejected (with JSON
128+
* body)</li>
129+
* <li>500 Internal Server Error - exception occurred</li>
130+
* </ul>
116131
* <p>
117132
* If you want custom exception handling and to set the return code,
118133
* implement this method and overwrite the default functionality.
119134
*
120135
* @param result
121136
* the result of the upload operation containing success status,
122-
* response object, and any exception that occurred
137+
* response object, any exception that occurred, and lists of
138+
* accepted/rejected files
123139
*/
124140
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());
141+
VaadinResponse response = result.response();
142+
try {
143+
if (result.exception() != null) {
144+
response.setStatus(
145+
HttpStatusCode.INTERNAL_SERVER_ERROR.getCode());
146+
} else if (result.allRejected()) {
147+
response.setStatus(422); // Unprocessable Entity
148+
response.setContentType("application/json");
149+
writeJsonResponse(response,
150+
new RejectedFilesResponse(result.rejectedFiles()));
151+
} else if (result.hasMixed()) {
152+
response.setStatus(207); // Multi-Status
153+
response.setContentType("application/json");
154+
writeJsonResponse(response, new MixedUploadResponse(
155+
result.acceptedFiles(), result.rejectedFiles()));
156+
} else {
157+
response.setStatus(HttpStatusCode.OK.getCode());
158+
}
159+
} catch (IOException e) {
160+
LoggerFactory.getLogger(UploadHandler.class)
161+
.error("Error writing upload response", e);
162+
response.setStatus(HttpStatusCode.INTERNAL_SERVER_ERROR.getCode());
163+
}
164+
}
165+
166+
private static void writeJsonResponse(VaadinResponse response,
167+
Object responseObject) throws IOException {
168+
ObjectMapper mapper = JacksonUtils.getMapper();
169+
try {
170+
String json = mapper.writeValueAsString(responseObject);
171+
PrintWriter writer = response.getWriter();
172+
writer.write(json);
173+
} catch (JacksonException e) {
174+
throw new IOException("Failed to serialize response to JSON", e);
130175
}
131176
}
132177

178+
/**
179+
* JSON response structure for rejected files.
180+
*/
181+
record RejectedFilesResponse(
182+
List<UploadResult.RejectedFile> rejected) implements
183+
java.io.Serializable {
184+
}
185+
186+
/**
187+
* JSON response structure for mixed upload results.
188+
*/
189+
record MixedUploadResponse(List<String> accepted,
190+
List<UploadResult.RejectedFile> rejected) 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)