Skip to content

Commit

Permalink
Handle parsing of form attributes with no value
Browse files Browse the repository at this point in the history
MicronautHttpData.AttributeImpl is updated to provide an implementation of the setValue method.
This implementation is needed in the corner cases where Netty's HttpPostStandardRequestDecoder
successfully parses an attribute with no value such as in the body "a&b=2".

Tests are added to more thoroughly test the various forms of x-www-form-urlencoded bodies as
specified by https://url.spec.whatwg.org/#application/x-www-form-urlencoded. These tests
include scenarios where we know and expect that HttpPostStandardRequestDecoder will currently
fail to parse an attribute with a name but no value - namely when that attribute is the last
one in a given POST body. If HttpPostStandardRequestDecoder is patched in the future, then
these expected parsing failures can be moved into the success scenario instead.

This partially resolves #10446.
  • Loading branch information
jeremyg484 committed Mar 12, 2024
1 parent 4fc430f commit d6bde2d
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 1 deletion.
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.toCharArray(), 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

0 comments on commit d6bde2d

Please sign in to comment.