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

Port v1 SDK customization for s3 HTTP PUT request #2051

Merged
merged 20 commits into from
Mar 21, 2023

Conversation

wty-Bryant
Copy link
Contributor

Add {Expect: 100-continue} http header customization middleware for s3 client HTTP PUT request larger than 2MB or with unknown-size body, solve #1451

Tianyi Wang added 3 commits March 9, 2023 18:04
@wty-Bryant wty-Bryant requested a review from a team as a code owner March 10, 2023 22:31

// returns true if service is either s3 or s3 control and needs s3 customization
private static boolean requiresS3Customization(Model model, ServiceShape service) {
return S3ModelUtils.isServiceS3(model, service) || S3ModelUtils.isServiceS3Control(model, service);
Copy link
Contributor

Choose a reason for hiding this comment

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

fix: Probably doesn't need to apply to s3control

public List<RuntimeClientPlugin> getClientPlugins() {
return ListUtils.of(
RuntimeClientPlugin.builder()
.servicePredicate(S3100Continue::requiresS3Customization)
Copy link
Contributor

Choose a reason for hiding this comment

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

fix: IIRC this builder can also specify an operation predicate, we should use that to restrict this to just the operations that we want to use it on (PutObject, UploadPart, etc)

config/go.mod Outdated
github.com/aws/aws-sdk-go-v2/service/sso v1.12.4
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4
github.com/aws/aws-sdk-go-v2/service/sts v1.18.5
github.com/aws/aws-sdk-go-v2 v1.17.6
Copy link
Contributor

Choose a reason for hiding this comment

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

fix: There are way too many changes to go.mod and go_module_metadata.go un-related to the intent of this PR. We should figure out why these changed/revert them. It makes it very difficult to follow the PR otherwise

return out, metadata, fmt.Errorf("unknown request type %T", req)
}

if req.Method == "PUT" && (req.ContentLength == -1 || (req.ContentLength == 0 && req.Body != nil) || req.ContentLength >= 1024*1024*2) {
Copy link
Contributor

Choose a reason for hiding this comment

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

comment: If we scope the customization correctly in codegen to only apply to specific operations we can probably remove the req.Method check here.

fix: Change limit (2MB) to a private constant in this file

)

// unit test for service/internal/s3shared/s3100continue.go
func TestAdd100ContinueHttpHeader(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

fix: Is there not a way to have the test live closer to where the code that defines it is (e.g. s3shared)? It would also seem like we don't really need to spinup a test server for this and can instead directly test the middleware (example from smithy-go).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I put it under s3shared containing middleware, circular dependencies occur since s3shared and s3 depends on each other in that case due to the test.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right but if you don't make the test depend on the s3 client and instead just test the middleware in isolation then it should be fine (see the example tests linked in previous comment)

* Add middleware, which adds {Expect: 100-continue} header for s3 client HTTP PUT request larger than 2MB
* or with unknown size streaming bodies, during operation builder step
*/
public class S3100Continue implements GoIntegration {
Copy link
Contributor

Choose a reason for hiding this comment

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

fix: Not sure where to make this comment but we ran into an issue with this header being set and signing. See this PR for more details. Can you verify that s3 put object requests with transfer acceleration enabled don't run into issues when this header is present? We may need to special case it in signing as we did in Kotlin.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm confused about the test design of this validation. Since UseAccelerate and SupportsAccelerate is configured in UpdateEndpoints at Serialize step in stack, is it possible to wire up inner middleware like processARNResource, s3ObjectLambdaEndpoint etc with current s3100Continue middleware in isolation in unit test to check issues?

Copy link
Contributor

Choose a reason for hiding this comment

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

No need to check it in a unit test. Just test the actual real S3 service with your customization enabled and UseAccelerate enabled (might need to configure it on the bucket).

@wty-Bryant wty-Bryant requested a review from aajtodd March 18, 2023 04:07
@@ -80,6 +80,11 @@ type Options struct {
// Configures the events that will be sent to the configured logger.
ClientLogMode aws.ClientLogMode

// The threshold ContentLength for HTTP PUT request to receive {Expect:
// 100-continue} header. When set to -1, this header will be opt out of the
// operation request; when set to 0, the thresholdwill be set to default 2MB
Copy link
Contributor

Choose a reason for hiding this comment

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

the threshold will (space)

Copy link
Contributor

@aajtodd aajtodd left a comment

Choose a reason for hiding this comment

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

Couple small fixes but this looks pretty close.

func TestIgnoredHeaders(t *testing.T) {
cases := map[string]struct {
Header string
ExpectIgnored bool
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Slightly confusing to understand the expectations due to the inversion. Originally interpreted the test cases as "expect the header to be ignored" but that isn't how it's used. Might consider renaming or changing the assertion code

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree, the initial ExcludeList rule within v4 is also a little confusing since it is valid when inner rule is invalid. I have changed the ExpectIgnored to be true if the header is ignored.

public List<RuntimeClientPlugin> getClientPlugins() {
return ListUtils.of(
RuntimeClientPlugin.builder()
.operationPredicate((model, service, operation) -> isS3Service(model, service) && operation.
Copy link
Contributor

Choose a reason for hiding this comment

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

fix: We probably really only care about the PutObject and UploadPart operations. The other APIs don't have large binary payloads where an Expect header makes any difference (and may not be supported by the service for those operations IDK).

Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Can service and operation predicates be combined such that you can move the isS3Service check to servicePredicate(...)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For service/operation predicates combination, if I put isS3Service within servicePredicate() , no matter I put that before or after the servicePredicate , it will put the add100Continue middleware within some operation that we don't want (outside s3 or within s3's other operation). Thus I think it might be better to keep the service check within operation predicate like S3GetBucketLocation

.type(SymbolUtils.createValueSymbolBuilder("int64")
.putProperty(SymbolUtils.GO_UNIVERSE_TYPE, true)
.build())
.documentation("The threshold ContentLength for HTTP PUT request to receive {Expect: 100-continue} header. " +
Copy link
Contributor

Choose a reason for hiding this comment

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

nit:

The threshold ContentLength in bytes ...

.putProperty(SymbolUtils.GO_UNIVERSE_TYPE, true)
.build())
.documentation("The threshold ContentLength for HTTP PUT request to receive {Expect: 100-continue} header. " +
"When set to -1, this header will be opt out of the operation request; when set to 0, the threshold" +
Copy link
Contributor

Choose a reason for hiding this comment

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

fix:

Setting to -1 will disable adding the Expect header to requests ...

return out, metadata, fmt.Errorf("unknown request type %T", req)
}

if req.ContentLength == -1 || (req.ContentLength == 0 && req.Body != nil) || req.ContentLength >= sizeLimit {
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: When do we hit req.ContentLength == 0 && req.Body != nil case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I haven't met such case in real code, it is judged only because the initial issue put forward that "Need to port the V1 SDK customization that set the Expect: 100-continue header for HTTP PUT operations that were larger 2 MB or streaming bodies where the content-length is unknown." According to http.Request comment about ContentLength, "For client requests, a value of 0 with a non-nil Body is also treated as unknown."

Copy link
Contributor

@aajtodd aajtodd left a comment

Choose a reason for hiding this comment

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

Fix and ship.

{
"id": "1d119df5-78fb-4a57-a293-4d38ea28aaa5",
"type": "feature",
"description": "enable s3100continue header config, exempt it from sigV4",
Copy link
Contributor

Choose a reason for hiding this comment

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

fix: There should only be one changelog entry for this entire PR/change.

private static final String ADD_100Continue_Header = "add100Continue";
private static final String ADD_100Continue_Header_INTERNAL = "Add100Continue";
private static final String Continue_Client_Option = "ContinueHeaderThresholdBytes";
private static final Set<String> Put_Op_Set = new HashSet<>(Arrays.asList("PutObject", "UploadPart"));
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Use the full shape ID rather than just the name (e.g. com.amazonaws.s3#PutObject or whatever it actually is).

)

const s3100ContinueID = "S3100Continue"
const defaultLimit int64 = 1024 * 1024 * 2
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: default100ContinueThresholdBytes or similar.

@wty-Bryant wty-Bryant merged commit c93b5cc into main Mar 21, 2023
@wty-Bryant wty-Bryant deleted the add100ContinueCustomization branch April 10, 2023 17:50
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.

None yet

3 participants