Skip to content
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

Handle parsing of form attributes with no value #10601

Merged
merged 3 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -513,7 +513,7 @@ public String getValue() throws IOException {

@Override
public void setValue(String value) throws IOException {
throw new UnsupportedOperationException();
setContent(Unpooled.copiedBuffer(value, factory.characterEncoding));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import io.micronaut.http.server.netty.AbstractMicronautSpec
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
import spock.lang.Issue
import spock.lang.Unroll

/**
* @author Graeme Rocher
Expand Down Expand Up @@ -76,6 +77,112 @@ class FormDataBindingSpec extends AbstractMicronautSpec {
e.response.status == HttpStatus.BAD_REQUEST
}

@Issue("https://github.com/micronaut-projects/micronaut-core/issues/10446")
@Unroll
void "test application/x-www-form-urlencoded String #body is parsed to #parsedMapString"() {
when:
String result = Flux.from(rxClient.exchange(HttpRequest.POST('/form/map', body)
.contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE), String)).blockFirst().getBody(String.class).get()

then:
result == parsedMapString

where:
body | parsedMapString
"a=1" | "[a:1]"
"a=1&b=2" | "[a:1, b:2]"

//nothing
"&a=1&b=2" | "[a:1, b:2]"
"a=1&&b=2" | "[a:1, b:2]"
"a=1&b=2&" | "[a:1, b:2]"

//key equals empty value
"z=&a=1&b=2" | "[z:, a:1, b:2]"
"a=1&z=&b=2" | "[a:1, z:, b:2]"
"a=1&b=2&z=" | "[a:1, b:2, z:]"

//empty key equals value
"=0&a=1&b=2" | "[:0, a:1, b:2]"
"a=1&=0&b=2" | "[a:1, :0, b:2]"
"a=1&b=2&=0" | "[a:1, b:2, :0]"

//empty key equals empty value
"=&a=1&b=2" | "[:, a:1, b:2]"
"a=1&=&b=2" | "[a:1, :, b:2]"
"a=1&b=2&=" | "[a:1, b:2, :]"

//just key
"z&a=1&b=2" | "[z:, a:1, b:2]"
"a=1&z&b=2" | "[a:1, z:, b:2]"
}

@Issue("https://github.com/micronaut-projects/micronaut-core/issues/10446")
@Unroll
void "test application/x-www-form-urlencoded String #body with single key fails to parse"() {
// NOTE - Parsing is expected to fail here unless HttpPostStandardRequestDecoder is corrected
when:
String result = Flux.from(rxClient.exchange(HttpRequest.POST('/form/map', body)
.contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE), String)).blockFirst().getBody(String.class).get()

then:
def ex = thrown(HttpClientResponseException)
ex.status == HttpStatus.BAD_REQUEST

where:
body | parsedMapString
//just key
"a" | "[a:]"
"&a" | "[a:]"
}

@Issue("https://github.com/micronaut-projects/micronaut-core/issues/10446")
@Unroll
void "test application/x-www-form-urlencoded String #body with single key as the last attribute fails to parse to #parsedMapString"() {
// NOTE - Parsing is expected to fail here unless HttpPostStandardRequestDecoder is corrected
when:
String result = Flux.from(rxClient.exchange(HttpRequest.POST('/form/map', body)
.contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE), String)).blockFirst().getBody(String.class).get()

then:
result != parsedMapString

where:
body | parsedMapString
//just key
"a=1&b=2&z" | "[a:1, b:2, z:]"
}

@Issue("https://github.com/micronaut-projects/micronaut-core/issues/10446")
@Unroll
void "test application/x-www-form-urlencoded Map #body is parsed to #parsedMapString"() {
//NOTE - Netty does not allow setting an http data attribute with an empty string key, which seems reasonable, thus fewer
// scenarios are exercised than the above that uses a raw String body. The key with empty value works here because it is
// sent as key= instead of just key, and Netty parses that correctly on the server end.
when:
String result = Flux.from(rxClient.exchange(HttpRequest.POST('/form/map', body)
.contentType(MediaType.APPLICATION_FORM_URLENCODED_TYPE), String)).blockFirst().getBody(String.class).get()

then:
result == parsedMapString

where:
body | parsedMapString
[a:"1"] | "[a:1]"
[a:"1", b:"2"] | "[a:1, b:2]"

//key equals empty value
[z:"",a:"1",b:"2"] | "[z:, a:1, b:2]"
[a:"1",z:"",b:"2"] | "[a:1, z:, b:2]"
[a:"1",b:"2",z:""] | "[a:1, b:2, z:]"

//just key
[z:"",a:"1",b:"2"] | "[z:, a:1, b:2]"
[a:"1",z:"",b:"2"] | "[a:1, z:, b:2]"
[a:""] | "[a:]"
[a:"1",b:"2",z:""] | "[a:1, b:2, z:]"
}

@Issue('https://github.com/micronaut-projects/micronaut-core/issues/1032')
void "test POST SAML form url encoded"() {
given:
Expand Down Expand Up @@ -179,6 +286,11 @@ class FormDataBindingSpec extends AbstractMicronautSpec {
"name: $person.name, age: $person.age"
}

@Post('/map')
String map(@Body Map<String, String> formData) {
formData.toMapString()
}

@EqualsAndHashCode
static class Person {
String name
Expand Down
Loading