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

HTTP 406 when request header includes "accept: */*" #1544

Open
mcmasn-amzn opened this issue Jul 8, 2022 · 10 comments
Open

HTTP 406 when request header includes "accept: */*" #1544

mcmasn-amzn opened this issue Jul 8, 2022 · 10 comments
Assignees
Labels
bug Something isn't working good first issue Good for newcomers server Rust server SDK

Comments

@mcmasn-amzn
Copy link
Contributor

It seems the generated server code is overly strict about requiring Accept: application/json. By default, clients like curl often send Accept: */*, but this produces a HTTP 406 response. This doesn't seem like the right behavior.

HTTP 1.1 RFC for Accept header - https://httpwg.org/specs/rfc7231.html#header.accept

Details

This occurs consistently with aws-smithy-http-server = "0.44". If I use the generated code for an simple service like this

Smithy

namespace test1
use aws.protocols#restJson1

@restJson1
service MySampleService {
    version: "2022-06-01",
    operations: [EmptyOperation]
}

@readonly
@http(uri: "/empty-operation", method: "GET")
operation EmptyOperation {
    input: EmptyOperationInput,
    output: EmptyOperationOutput,
}

@input
structure EmptyOperationInput { }

@output
structure EmptyOperationOutput { }

I believe the origin of the problem is the generated code I found in operation.rs


#[derive(Debug)]
pub(crate) struct EmptyOperationOperationInputWrapper(crate::input::EmptyOperationInput);
#[async_trait::async_trait]
impl<B> aws_smithy_http_server::request::FromRequest<B> for EmptyOperationOperationInputWrapper
where
    B: aws_smithy_http_server::body::HttpBody + Send, 
    B::Data: Send,
    aws_smithy_http_server::rejection::RequestRejection : From<<B as aws_smithy_http_server::body::HttpBody>::Error>
{
    type Rejection = aws_smithy_http_server::runtime_error::RuntimeError;
    async fn from_request(req: &mut aws_smithy_http_server::request::RequestParts<B>) -> Result<Self, Self::Rejection> {
        if let Some(headers) = req.headers() {
                        if let Some(accept) = headers.get(http::header::ACCEPT) {
                            if accept != "application/json" {
                                return Err(Self::Rejection {
                                    protocol: aws_smithy_http_server::protocols::Protocol::RestJson1,
                                    kind: aws_smithy_http_server::runtime_error::RuntimeErrorKind::NotAcceptable,
                                })
                            }
                        }
                    }
        crate::operation_deser::parse_empty_operation_request(req)
            .await
            .map(EmptyOperationOperationInputWrapper)
            .map_err(
                |err| aws_smithy_http_server::runtime_error::RuntimeError {
                    protocol: aws_smithy_http_server::protocols::Protocol::RestJson1,
                    kind: err.into()
                }
            )
    }
}

Fails with Accept: */*

curl -v http://localhost:3000/empty-operation
*   Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /empty-operation HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.79.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 406 Not Acceptable
< content-type: application/json
< x-amzn-errortype: NotAcceptableException
< content-length: 2
< date: Fri, 08 Jul 2022 21:44:02 GMT
< 
* Connection #0 to host localhost left intact
{}%    

Passes with Accept: application/json

curl -H 'Accept: application/json'  -v http://localhost:3000/empty-operation
*   Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /empty-operation HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.79.1
> Accept: application/json
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< content-type: application/json
< content-length: 2
< date: Fri, 08 Jul 2022 21:49:51 GMT
< 
* Connection #0 to host localhost left intact
{}
@jamesls
Copy link

jamesls commented Jul 8, 2022

Oh nice, I just ran into this exact same issue.

To give a concrete use case, I am unable to use the generated typescript clients from smithy-typescript in the browser, they fail with a 406 response because they send Accept: */*.

On whatever end this gets fixed, it seems like the smithy-typescript clients should be able to talk to the smithy-rs servers when generated against the same model.

@dmcallas
Copy link

Just to add in one more failing case: the client I'm using sends accept:application/json, text/javascript, */*. Even though this contains the correct application/json type, it also returns a 406 error since the generated server code is looking for an exact string match with application/json.

@david-perez david-perez added bug Something isn't working server Rust server SDK good first issue Good for newcomers labels Aug 3, 2022
@david-perez
Copy link
Contributor

I'm thinking that to fix this, we could implement a tower::Layer that correctly rejects requests when the Accept header does not include a configured value. This parsing logic is beneficial to the entire Rust Tower community, not only smithy-rs users.

@82marbag
Copy link
Contributor

With the PR to tower http, it would become like this. Should we still have the check on our side, instead of asking users to add that layer manually?

@david-perez
Copy link
Contributor

@82marbag We can't rely on users wrapping their Smithy service in the appropriate layer. It's also something that needs to be applied in codegen at the operation level. My idea is that we eventually vend it as middleware, which implements both tower::Layer and tower::Service. External users would apply the layer. In smithy-rs, we would call is_ready and call on the Service right where we're performing the current check.

We'd also need to map from whatever response the Service returns into the smithy-rs response body type, honoring the protocol too, which would be tricky.

Take the RequestBodyLimit middleware from tower_http as an example; it's likely that our middleware would be very similar, since both middlewares reject requests based on an HTTP header. If you look at the Service implementation, notice that if the request is rejected, a ResponseFuture::payload_too_large() response is returned, which just returns a text response with body length limit exceeded and a 413 status code. smithy-rs can't use an off-the-shelf layer like this one, it has additional requirements: it needs to return a protocol-specific response.

The more I think about it the more I think we should just have a simple fn accept_header_classifier(req: http:Request<B>, content_type: String) -> bool that says whether the request should be rejected in smithy-rs. The Tower middleware could then be written in terms of that logic.

@82marbag 82marbag mentioned this issue Aug 18, 2022
2 tasks
@82marbag
Copy link
Contributor

Fixed now, you can pull from main or wait for the next release. Keeping this open for future refactoring with tower

@david-perez
Copy link
Contributor

Can we add some protocol tests?

@82marbag
Copy link
Contributor

smithy-lang/smithy#1365, #1648

@david-perez

This comment was marked as off-topic.

@david-perez

This comment was marked as off-topic.

@82marbag 82marbag self-assigned this Sep 30, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working good first issue Good for newcomers server Rust server SDK
Projects
None yet
Development

No branches or pull requests

5 participants