Skip to content

Conversation

@plotnick
Copy link
Contributor

@plotnick plotnick commented Jun 4, 2022

This is useful for compatibility with protocols such as OAuth 2.0 and SAML, which frequently use this encoding for request bodies. It is possible to work around this by using UntypedBody and decoding at the application level, but handling it in dropshot seems safer and less error-prone.

Copy link
Collaborator

@ahl ahl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should our approach be with regard to the OpenAPI description of APIs? Currently we claim they accept application/json only. I don't really want to say that they accept application/x-www-form-urlencoded as well, only because it uglies things up (and will fuck up progenitor, but that's my problem). I'm fine with just having it be un-expressed in OpenAPI, but wanted to get your opinion.

@david-crespo
Copy link
Contributor

What should our approach be with regard to the OpenAPI description of APIs? Currently we claim they accept application/json only. I don't really want to say that they accept application/x-www-form-urlencoded as well, only because it uglies things up (and will fuck up progenitor, but that's my problem). I'm fine with just having it be un-expressed in OpenAPI, but wanted to get your opinion.

Does this make every endpoint accept form-urlencoded or does it let us mark particular ones as accepting it? Seems like the former. I do think it should be in the spec, which is why it's a shame that this affects all endpoints — it will add a lot of noise. Ideally IMO it would a) be in the spec, but b) only on a few relevant endpoints.

I think the TS generator will be unaffected because we're specifically looking for the application/json key. If there was an endpoint that only took form-urlencoded and not json I might have to add a case for that (for now, probably by skipping such endpoints since I don't think the console needs to hit them).

@plotnick
Copy link
Contributor Author

plotnick commented Jun 8, 2022

Does this make every endpoint accept form-urlencoded or does it let us mark particular ones as accepting it? ... Ideally IMO it would a) be in the spec, but b) only on a few relevant endpoints.

The current implementation does indeed make every endpoint accept form-urlencoded bodies, and I see now from your and @ahl's comments that that's not really the right approach. I will revise this so that it's opt-in and properly marked in the spec. Thank you both!

@ahl
Copy link
Collaborator

ahl commented Jun 8, 2022

The current implementation does indeed make every endpoint accept form-urlencoded bodies, and I see now from your and @ahl's comments that that's not really the right approach. I will revise this so that it's opt-in and properly marked in the spec. Thank you both!

I think we have some options. I don't know what endpoints are going to use this behavior--do we want them in the OpenAPI spec? How much do we care that an endpoint must use application/json if the client happens to do form-urlencode?

In short: I think we can be a little loosey goosey here without significant consequence... especially in light of competing priorities.

If elided, defaults to "application/json". The requested content type
is verified against the specified one by the handler.
@plotnick
Copy link
Contributor Author

I took updating this as a background task, and after a few tries I think I have something (c903eb8) that satisfies the desiderata. I'm not wild about needing to pass the content-type into the Extractor::metadata method, but I could not find any way around it; I think everything else should be more or less fine, but feedback (as always) would be welcome.

@plotnick plotnick requested a review from ahl June 21, 2022 18:51
Copy link
Collaborator

@ahl ahl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking good. thanks for making the changes

two questions?
Are we going to use this in a place where we'll want progenitor support?

How does this work for nested structs? Looking briefly at the serde_urlencoded crate I think it produces a runtime error. Might we want to look at the schema for bodies to give a friendly error when the server starts?

Comment on lines +86 to +87
/// // Specifies the media type used to encode the request body
/// content_type = { "application/json" | "application/x-www-form-urlencoded" }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is the best approach. The alternative I could think of is to have a wrapper type so that endpoints look something like..

async fn demo_handler_args_2urlencoded(
    _rqctx: RequestCtx,
    body: TypedBody<DemoJsonBody, FormUrlEncodedMarkerTypeOrWhatever>,
) -> Result<Response<Body>, HttpError> {
    // ...
}

I don't think that's actually better--it just moves the metadata from the macro to the type. It would be both messier and harder to see.

@davepacheco do you have any thoughts here?

Comment on lines +124 to +125
let content_type =
metadata.content_type.unwrap_or_else(|| "application/json".to_string());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we validate that this is application/x-www-form-urlencoded? What do you think about a trybuild test (the bad_endpointNNN.rs tests) to catch an invalid content type at build time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, ApiEndpointBodyContentType::from_mime_type raises an error on mime types that it doesn't recognize, but you're right that it's not at build time. I thought we should have only one place that knows which types are acceptable, but was hesitant to move ApiEndpointBodyContentType and the associated constants into dropshot_endpoint. fdc22c8 adds a simple check at build time and a new bad_endpoint16.rs test, but does duplicate the list of acceptable types.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good; two small nits, but up to you if you want to change them

Comment on lines +181 to +182
/** application/x-www-form-urlencoded */
UrlEncoded,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we care that the user says "application/x-www-form-urlencoded" or could we just let them give us any string and have the variant be Yolo(String)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In all but the UntypedBody case, we do care because we need to know how to decode the body.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry: I know we care what it is for decoding, but can we stuff that into a string rather than having Json and UrlEncoded as the two enumerated options?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm, we could, but I'm not sure I see the advantage. We could certainly add another Yolo variant for arbitrary types, but since these are the ones that we handle specially I think it makes sense to have variants for them. Otherwise why not just use strings for content type everywhere?

@plotnick
Copy link
Contributor Author

Are we going to use this in a place where we'll want progenitor support?

In the immediate use case I have, we won't need progenitor to handle the urlencoded endpoints (since it's for OAuth, for which we'll use third-party libs). So... maybe?

How does this work for nested structs?

That's an excellent question; I hadn't thought of that at all. Is there an easy way to check for those? And could we take that as a follow-up?

@ahl
Copy link
Collaborator

ahl commented Jun 22, 2022

How does this work for nested structs?

That's an excellent question; I hadn't thought of that at all. Is there an easy way to check for those? And could we take that as a follow-up?

I'm fine with this being just filed as an issue for future work.

I think the way to do it would be to look at the JsonSchema and make sure it's an object whose properties are all scalars.

@plotnick
Copy link
Contributor Author

maybe we just return here to avoid the extra boilerplate?

Yep, we sure can; thanks! Fixed in d1963f7.

@plotnick
Copy link
Contributor Author

I don't think this is valid, is it?

Indeed not, thank you. Also fixed in d1963f7.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants