mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 16:39:29 +00:00
Emit multipart parts with empty bodies in PartGenerator
Parts were emitted only from PartListener.onBody(buffer, last=true), so a part with an empty body (for example a blank form field, or a trailing empty part) was silently dropped from the resulting MultiValueMap, and was indistinguishable from an absent field. This carries over the fix from the reactive DefaultPartHttpMessageReader (spring-framework#30953): State gains an onComplete() callback that emits the part, also when it has an empty body. It is invoked when a new part begins, and when parsing completes for the final part. Signed-off-by: Sagar Chanchal <Sagarr2112@gmail.com>
This commit is contained in:
committed by
Brian Clozel
parent
2028c54d01
commit
8f4fcb6cbc
+39
@@ -86,6 +86,7 @@ final class PartGenerator implements MultipartParser.PartListener {
|
||||
|
||||
@Override
|
||||
public void onHeaders(HttpHeaders headers) {
|
||||
this.state.onComplete();
|
||||
if (isFormField(headers)) {
|
||||
this.state = new FormFieldState(headers);
|
||||
}
|
||||
@@ -127,6 +128,7 @@ final class PartGenerator implements MultipartParser.PartListener {
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
this.state.onComplete();
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Finished reading " + this.partCount + " part(s)");
|
||||
}
|
||||
@@ -171,6 +173,14 @@ final class PartGenerator implements MultipartParser.PartListener {
|
||||
*/
|
||||
void onBody(DataBuffer dataBuffer, boolean last);
|
||||
|
||||
/**
|
||||
* Invoked when no further {@link #onBody(DataBuffer, boolean) body} is
|
||||
* expected for the current part, that is when a new part begins, or when
|
||||
* parsing completed. This emits the part, also when it has an empty body.
|
||||
*/
|
||||
default void onComplete() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources.
|
||||
*/
|
||||
@@ -210,6 +220,8 @@ final class PartGenerator implements MultipartParser.PartListener {
|
||||
|
||||
private final HttpHeaders headers;
|
||||
|
||||
private boolean emitted;
|
||||
|
||||
public FormFieldState(HttpHeaders headers) {
|
||||
this.headers = headers;
|
||||
}
|
||||
@@ -227,6 +239,14 @@ final class PartGenerator implements MultipartParser.PartListener {
|
||||
PartGenerator.this.maxInMemorySize + " bytes");
|
||||
}
|
||||
if (last) {
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
if (!this.emitted) {
|
||||
this.emitted = true;
|
||||
byte[] bytes = this.value.toByteArrayUnsafe();
|
||||
String value = new String(bytes, MultipartUtils.charset(this.headers));
|
||||
FormFieldPart formFieldPart = DefaultParts.formFieldPart(this.headers, value);
|
||||
@@ -268,6 +288,7 @@ final class PartGenerator implements MultipartParser.PartListener {
|
||||
|
||||
private final HttpHeaders headers;
|
||||
|
||||
private boolean emitted;
|
||||
|
||||
public InMemoryState(HttpHeaders headers) {
|
||||
this.headers = headers;
|
||||
@@ -282,6 +303,14 @@ final class PartGenerator implements MultipartParser.PartListener {
|
||||
}
|
||||
this.content.add(dataBuffer);
|
||||
if (last) {
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
if (!this.emitted) {
|
||||
this.emitted = true;
|
||||
emitMemoryPart();
|
||||
}
|
||||
}
|
||||
@@ -339,6 +368,8 @@ final class PartGenerator implements MultipartParser.PartListener {
|
||||
|
||||
private long byteCount;
|
||||
|
||||
private boolean emitted;
|
||||
|
||||
public FileState(HttpHeaders headers, Path folder) {
|
||||
this.headers = headers;
|
||||
this.file = createFile(folder);
|
||||
@@ -377,6 +408,14 @@ final class PartGenerator implements MultipartParser.PartListener {
|
||||
}
|
||||
writeBuffer(dataBuffer);
|
||||
if (last) {
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
if (!this.emitted) {
|
||||
this.emitted = true;
|
||||
Part part = DefaultParts.part(this.headers, this.file);
|
||||
PartGenerator.this.addPart(part);
|
||||
closeOutputStream();
|
||||
|
||||
+20
@@ -196,6 +196,26 @@ class MultipartHttpMessageConverterTests {
|
||||
assertThat(result.get("text2")).anyMatch(isFormData("text2", "b"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readMultipartEmptyPart() throws Exception {
|
||||
MockHttpInputMessage response = createMultipartResponse("servlet-empty-part.multipart", "boundary");
|
||||
MultiValueMap<String, Part> result = converter.read(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class), response, null);
|
||||
|
||||
assertThat(result).containsOnlyKeys("text1", "text2");
|
||||
assertThat(result.get("text1")).anyMatch(isFormData("text1", ""));
|
||||
assertThat(result.get("text2")).anyMatch(isFormData("text2", "a"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readMultipartEmptyLastPart() throws Exception {
|
||||
MockHttpInputMessage response = createMultipartResponse("servlet-empty-last-part.multipart", "boundary");
|
||||
MultiValueMap<String, Part> result = converter.read(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class), response, null);
|
||||
|
||||
assertThat(result).containsOnlyKeys("text1", "text2");
|
||||
assertThat(result.get("text1")).anyMatch(isFormData("text1", "a"));
|
||||
assertThat(result.get("text2")).anyMatch(isFormData("text2", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readMultipartInvalid() throws Exception {
|
||||
MockHttpInputMessage response = createMultipartResponse("garbage-1.multipart", "boundary");
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
--boundary
|
||||
Content-Disposition: form-data; name="text1"
|
||||
|
||||
a
|
||||
--boundary
|
||||
Content-Disposition: form-data; name="text2"
|
||||
|
||||
|
||||
--boundary--
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
--boundary
|
||||
Content-Disposition: form-data; name="text1"
|
||||
|
||||
|
||||
--boundary
|
||||
Content-Disposition: form-data; name="text2"
|
||||
|
||||
a
|
||||
--boundary--
|
||||
Reference in New Issue
Block a user