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

Refactor: Stop Splitting Header Values #121

Merged
merged 10 commits into from
Nov 8, 2023

Conversation

Johannes-Schneider
Copy link
Contributor

@Johannes-Schneider Johannes-Schneider commented Nov 7, 2023

Context

SAP/cloud-sdk-java-backlog#156.
SAP/cloud-sdk-java-backlog/discussions/343

This PR changes our RequestHeaderContainer and the ODataRequestResult to no longer split header values.

Feature scope:

  • Adjust RequestHeaderContainer
    • JavaDoc
    • Implementation (DefaultRequestHeaderContainer)
  • Adjust ODataRequestResult

Definition of Done

@Johannes-Schneider Johannes-Schneider added please merge Request to merge a pull request please review Request to review a pull request labels Nov 7, 2023
@Johannes-Schneider Johannes-Schneider marked this pull request as ready for review November 7, 2023 12:46

final RequestHeaderContainer sut = DefaultRequestHeaderContainer.fromMultiValueMap(input);

assertThat(sut.getHeaderValues("Header1")).containsExactlyInAnyOrder(" Value1-1 ");
Copy link
Contributor

Choose a reason for hiding this comment

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

(Comment)

I'm wondering whether it'd be worth it investigating how Spring/Apache/JDK11 HttpClient is handling white spaces in response header values-

Copy link
Contributor Author

@Johannes-Schneider Johannes-Schneider Nov 7, 2023

Choose a reason for hiding this comment

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

TL;DR

  • Spring does NOT process header values in any way
  • Apache DOES trim header values
Spring Header Handling
public class HttpHeadersLearningTest {
    @Test
    public void testGetSimple() {
        final HttpHeaders sut = new HttpHeaders();
        sut.add("name", "value1, value2");

        assertThat(sut.get("name")).containsExactlyInAnyOrder("value1, value2");
    }

    @Test
    public void testAddEmptyName() {
        final HttpHeaders sut = new HttpHeaders();
        sut.add("", "value");

        assertThat(sut.keySet()).containsExactlyInAnyOrder("");
    }

    @Test
    public void testGetValueWithTrailingSpaces() {
        final HttpHeaders sut = new HttpHeaders();
        sut.add("name", "  value\t");

        assertThat(sut.get("name")).containsExactlyInAnyOrder("  value\t");
    }

    @Test
    public void testGetValueIgnoresKeyCasing() {
        final HttpHeaders sut = new HttpHeaders();
        sut.add("name", "value");

        assertThat(sut.get("name")).containsExactlyInAnyOrder("value");
        assertThat(sut.get("NAME")).containsExactlyInAnyOrder("value");
    }
}
Apache Header Handling
public class ApacheHeaderLearningTest {
    @Test
    public void testGetElementsSimple() {
        final BasicHeader sut = new BasicHeader("name", "value1, value2");

        assertThat(Arrays.stream(sut.getElements()).map(HeaderElement::getName))
                .containsExactlyInAnyOrder("value1", "value2");
    }

    @Test
    public void testGetElementsCookies() {
        final BasicHeader sut = new BasicHeader("Set-Cookie", "cookie1; cookie2");

        assertThat(sut.getElements())
                .containsExactly(
                        new BasicHeaderElement(
                                "cookie1",
                                null,
                                new NameValuePair[]{new BasicNameValuePair("cookie2", null)}));
    }

    @Test
    public void testGetElementsQuoted() {
        final BasicHeader sut = new BasicHeader("name", "\"value1, value2\", value3");

        assertThat(Arrays.stream(sut.getElements()).map(HeaderElement::getName))
                .containsExactlyInAnyOrder("\"value1", "value2\"", "value3");
    }

    @Test
    public void testGetElementsSingleQuoted() {
        final BasicHeader sut = new BasicHeader("name", "'value1, value2', value3");

        assertThat(Arrays.stream(sut.getElements()).map(HeaderElement::getName))
                .containsExactlyInAnyOrder("'value1", "value2'", "value3");
    }

    @Test
    public void testGetElementsWithEmptyEntry() {
        final BasicHeader sut = new BasicHeader("name", "value1, , value3");

        assertThat(Arrays.stream(sut.getElements()).map(HeaderElement::getName)).containsExactly("value1", "value3");
    }

    @Test
    public void testGetElementsWithTrailingSpaces() {
        final BasicHeader sut = new BasicHeader("name", "  value\t");

        assertThat(Arrays.stream(sut.getElements()).map(HeaderElement::getName)).containsExactly("value");
    }
}


@SafeVarargs
@Nonnull
private static HttpResponse mockResponseWithHeaders(
Copy link
Contributor

Choose a reason for hiding this comment

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

(Minor)

Feels like this method can be nicely reduced by just using implementation BasicHttpResponse with respective string-based header-methods.

newtork
newtork previously approved these changes Nov 7, 2023
Copy link
Contributor

@newtork newtork left a comment

Choose a reason for hiding this comment

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

Only minor comments, otherwise LGTM

Copy link
Member

@cschubertcs cschubertcs left a comment

Choose a reason for hiding this comment

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

lgtm!
When we offer the same public API we should also have a look the concrete implementation. Best to use the same kind of Map (ImmutableListMultimap with "normalized" keys vs. TreeMap with String::compareToIgnoreCase). But for the moment that should result in the same behavior.

Co-authored-by: Alexander Dümont <alexander.duemont@sap.com>
@@ -32,15 +31,8 @@
@Beta
@EqualsAndHashCode
@RequiredArgsConstructor( access = AccessLevel.PRIVATE )
public class DefaultRequestHeaderContainer implements RequestHeaderContainer
public final class DefaultRequestHeaderContainer implements RequestHeaderContainer
Copy link
Contributor

Choose a reason for hiding this comment

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

(Question)

Why final?

Copy link
Contributor Author

@Johannes-Schneider Johannes-Schneider Nov 8, 2023

Choose a reason for hiding this comment

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

As discussed, the constructor is private so the class cannot be inherited (in productive code).

For mocking (i.e. during tests), I'd expect customers to either mock the interface (RequestHeaderContainer) or to use an actual (i.e. non-mocked) instance of the implementation (DefaultRequestHeaderContainer).
Therefore, I'd argue this change to be fine.


Besides: This guide seems to hint that mocking final classes is not a problem at all.

Copy link
Contributor

@newtork newtork left a comment

Choose a reason for hiding this comment

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

LGTM

Very minor, map implementation choice as Chris mentioned. Minor code quality.

@Johannes-Schneider Johannes-Schneider merged commit 1b86d93 into main Nov 8, 2023
15 checks passed
@Johannes-Schneider Johannes-Schneider deleted the bli-156/stop-splitting-header-values branch November 8, 2023 11:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
please merge Request to merge a pull request please review Request to review a pull request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants