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

Fix request-compression stream handling #822

Merged
merged 5 commits into from Nov 19, 2019
Merged

Conversation

lukasniemeier-zalando
Copy link
Member

This is an attempt to fix the broken RequestCompressionPlugin.

Description

  • ensure compressing stream wrapper is closed, hence compressors are flushed
  • handle streaming output (also move header manipulation up to be visible in streaming mode)
  • ensure compression stream is wrapped only once (vs. each time a writer to the output message calls getBody)

Also...

  • extracted the plugin to it's own artifact to avoid module cycles
  • extended the tests to actually verify (de-)compression for a set of
    request factories

Types of changes

  • Bug fix
  • New feature
  • Breaking change

Checklist:

  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.

* ensure compressing stream wrapper is closed, hence compressors are
flushed
* handle streaming output (also move header manipulation up to be
visible in streaming mode)
* ensure compression stream is wrapped only once (vs. each time a writer
to the output message calls `getBody`)

Also...

* extracted the plugin to it's own artifact to avoid module cycles
* extended the tests to actually verify (de-)compression for a set of
request factories

@AllArgsConstructor
@Getter
private static final class DelegatingHttpOutputMessage implements HttpOutputMessage {
Copy link
Member Author

Choose a reason for hiding this comment

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

At least in the LogbookPlugin this class already exists

Copy link
Collaborator

Choose a reason for hiding this comment

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

It's just a couple of lines. No need to couple them together by trying to reuse something.

Copy link
Member

Choose a reason for hiding this comment

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

Why not to move them to core?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Not worth having this kind of coupling just to save 3 lines of code.

}

private void writeToCompressing(HttpOutputMessage message) throws IOException {
try (final WrappingHttpOutputMessage compressingMessage =
Copy link
Member Author

Choose a reason for hiding this comment

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

I am not sure whether this is the right time to close the output stream, the interfaces of Spring are very confusing to me. It seems to work though looking at the integration tests.

import java.io.IOException;
import java.io.OutputStream;

final class WrappingHttpOutputMessage implements HttpOutputMessage, AutoCloseable {
Copy link
Member Author

Choose a reason for hiding this comment

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

I'd not be surprised if such class already exists somewhere in this project

update(message.getHeaders());

if (message instanceof StreamingHttpOutputMessage) {
final StreamingHttpOutputMessage streaming = (StreamingHttpOutputMessage) message;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm thinking whether this could be simplified and done only once for all entities, since it's always the same pattern.


@API(status = EXPERIMENTAL)
@AllArgsConstructor(staticName = "of")
@Getter
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of Getter it could expose a decorate method directly

Copy link
Member Author

Choose a reason for hiding this comment

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

@AllArgsConstructor(staticName = "of")
public final class Compression {
    @Getter
    private final String contentEncoding;
    private final ThrowingUnaryOperator<OutputStream, IOException> outputStreamDecorator;

    public OutputStream compress(final OutputStream stream) throws IOException {
        return outputStreamDecorator.tryApply(stream);
    }
}

Like this?

Working with contentEncoding may still be done by RequestCompressionPlugin (via the getter on Compression), or also the check for arguments.getHeaders().containsKey(CONTENT_ENCODING) should move IMO,

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe something like this?

public interface Compression {

    public static final Compression GZIP = new DefaultCompression("gzip", GzipOutputStream::new);

    void update(HttpHeaders headers);
    OutputStream decorate(OutputStream stream) throws IOException;

}

Copy link
Member Author

Choose a reason for hiding this comment

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

What do you think of:

Working with contentEncoding may still be done by RequestCompressionPlugin (via the getter on Compression), or also the check for arguments.getHeaders().containsKey(CONTENT_ENCODING) should move IMO,

Is related to your comment in #822 (comment)

private final ThrowingUnaryOperator<OutputStream, IOException> wrapper;
private OutputStream stream;

WrappingHttpOutputMessage(HttpOutputMessage message, ThrowingUnaryOperator<OutputStream, IOException> wrapper) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

@AllArgsConstructor?

Copy link
Member Author

Choose a reason for hiding this comment

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

OutputStream stream is not injected, but managed internally.


### Limitations

* You must only configure a single `RequestCompressionPlugin` as only a single encoding is applied currently.
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 reworded since you can adding multiple, but only the first one takes effect.

@Override
public RequestExecution aroundNetwork(final RequestExecution execution) {
return arguments -> {
final Entity entity = arguments.getEntity();
Copy link
Collaborator

Choose a reason for hiding this comment

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

final boolean alreadyCompressed = arguments.getHeaders().containsKey(CONTENT_ENCODING);

if (alreadyCompressed) {
    return execution.execute(arguments);
}

final Entity entity = arguments.getEntity();

if (entity.isEmpty()) {
    return execution.execute(arguments);
}

return ...;

@Override
public OutputStream getBody() throws IOException {
if (stream == null) {
stream = wrapper.apply(message.getBody());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually, this whole class is only used within writeToCompressing, i.e. it's safe to decorate the stream early on since we can be sure it will be called.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, it'll be on your head! 😉

void shouldCompressRequestBody(final ClientHttpRequestFactory factory) {
driver.addExpectation(onRequestTo("/")
.withMethod(POST)
.withHeader("X-Content-Encoding", "gzip") // written by Jetty's GzipHandler
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is Jetty removing the original header and adds this one to indicate it was compressed?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I did not want to put some listener/callback mechanism in my (un-)gzipping rest client driver Jetty hack for now...

@whiskeysierra
Copy link
Collaborator

👍

@whiskeysierra whiskeysierra merged commit 619c71b into master Nov 19, 2019
@whiskeysierra whiskeysierra deleted the compression-stream branch November 19, 2019 20:09
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