Skip to content

Commit aaa9169

Browse files
authored
impl(rest): dedup and merge headers right before writing them to curl (#15100)
1 parent 6e8c2df commit aaa9169

File tree

8 files changed

+313
-35
lines changed

8 files changed

+313
-35
lines changed

google/cloud/internal/ci/run_integration_tests_emulator_bazel.sh

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ shift
3030
BAZEL_VERB="$1"
3131
shift
3232

33-
if bazel::has_no_tests "//google/cloud:all"; then
34-
exit 0
35-
fi
36-
3733
# Separate caller-provided excluded targets (starting with "-//..."), so that
3834
# we can make sure those appear on the command line after `--`.
3935
bazel_test_args=()

google/cloud/internal/curl_impl.cc

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -237,29 +237,70 @@ CurlImpl::~CurlImpl() {
237237
factory_->CleanupMultiHandle(std::move(multi_), HandleDisposition::kKeep);
238238
}
239239

240-
void CurlImpl::SetHeader(std::string const& header) {
240+
void CurlImpl::SetHeader(HttpHeader header) {
241241
if (header.empty()) return;
242242

243243
// The API for credentials is complicated, and the authorization
244-
// header can be empty. See, for example, AnonymousCredentials.
245-
if (header == "authorization: ") return;
244+
// header can be empty, but we don't actually want to send it. See, for
245+
// example, AnonymousCredentials.
246+
if (header.IsSameKey("authorization") && header.EmptyValues()) {
247+
return;
248+
}
249+
250+
pending_request_headers_.push_back(std::move(header));
251+
}
246252

253+
void CurlImpl::WriteHeader(std::string const& header) {
247254
auto* headers = curl_slist_append(request_headers_.get(), header.c_str());
248255
(void)request_headers_.release(); // Now owned by list, not us.
249256
request_headers_.reset(headers);
250257
}
251258

252-
void CurlImpl::SetHeader(std::pair<std::string, std::string> const& header) {
253-
SetHeader(absl::StrCat(header.first, ": ", header.second));
254-
}
259+
void CurlImpl::MergeAndWriteHeaders(
260+
std::function<void(HttpHeader const&)> const& write_fn) {
261+
// There are some headers that we do not want to merge. These headers
262+
// could have been added via custom user headers or due to bugs in the SDK.
263+
static constexpr std::array<char const*, 2> kDoNotMergeHeaderKeys{
264+
"authorization", "content-length"};
265+
std::stable_sort(pending_request_headers_.begin(),
266+
pending_request_headers_.end());
267+
268+
auto current = pending_request_headers_.begin();
269+
while (current != pending_request_headers_.end()) {
270+
// If this is the last header, write it and stop.
271+
if (current + 1 == pending_request_headers_.end()) {
272+
write_fn(*current);
273+
break;
274+
}
255275

256-
void CurlImpl::SetHeaders(RestContext const& context,
257-
RestRequest const& request) {
258-
for (auto const& header : context.headers()) {
259-
SetHeader(std::make_pair(header.first, absl::StrJoin(header.second, ",")));
276+
// Look ahead to see if there is another header with the same key. If so,
277+
// merge its values into this header until all headers of the same key are
278+
// merged.
279+
auto next = current + 1;
280+
while (next != pending_request_headers_.end() &&
281+
current->IsSameKey(*next)) {
282+
if (internal::ContainsIf(
283+
kDoNotMergeHeaderKeys,
284+
[&current](char const* h) { return current->IsSameKey(h); })) {
285+
GCP_LOG(WARNING) << "Ignoring duplicate header: "
286+
<< next->DebugString();
287+
} else {
288+
current->MergeHeader(*next);
289+
}
290+
++next;
291+
}
292+
293+
// Write the merged (or not merged) header and proceed to the next header
294+
// key.
295+
write_fn(*current);
296+
current = next;
260297
}
261-
for (auto const& header : request.headers()) {
262-
SetHeader(std::make_pair(header.first, absl::StrJoin(header.second, ",")));
298+
}
299+
300+
void CurlImpl::SetHeaders(
301+
std::unordered_map<std::string, std::vector<std::string>> const& headers) {
302+
for (auto const& header : headers) {
303+
SetHeader(HttpHeader(header.first, header.second));
263304
}
264305
}
265306

@@ -427,7 +468,12 @@ Status CurlImpl::MakeRequest(HttpMethod method, RestContext& context,
427468
if (!status.ok()) return OnTransferError(context, std::move(status));
428469
status = handle_.SetOption(CURLOPT_SEEKDATA, &writev_);
429470
if (!status.ok()) return OnTransferError(context, std::move(status));
430-
SetHeader("Expect:");
471+
// Adding an empty Expect header instructs libcurl never to attempt to
472+
// insert an "Expect: 100-continue" header per its own logic. When libcurl
473+
// adds this header/value pair it introduces another round trip before data
474+
// is sent. Additionally, some webservers do not handle the 100-continue
475+
// correctly causing the request to fail.
476+
SetHeader(HttpHeader("Expect"));
431477
return MakeRequestImpl(context);
432478
}
433479

@@ -538,6 +584,8 @@ Status CurlImpl::MakeRequestImpl(RestContext& context) {
538584
Status status;
539585
status = handle_.SetOption(CURLOPT_URL, url_.c_str());
540586
if (!status.ok()) return OnTransferError(context, std::move(status));
587+
MergeAndWriteHeaders(
588+
[this](HttpHeader const& h) { WriteHeader(std::string(h)); });
541589
status = handle_.SetOption(CURLOPT_HTTPHEADER, request_headers_.get());
542590
if (!status.ok()) return OnTransferError(context, std::move(status));
543591
status = handle_.SetOption(CURLOPT_USERAGENT, user_agent_.c_str());

google/cloud/internal/curl_impl.h

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "google/cloud/internal/curl_handle_factory.h"
2020
#include "google/cloud/internal/curl_wrappers.h"
2121
#include "google/cloud/internal/curl_writev.h"
22+
#include "google/cloud/internal/http_header.h"
2223
#include "google/cloud/internal/rest_context.h"
2324
#include "google/cloud/internal/rest_request.h"
2425
#include "google/cloud/internal/rest_response.h"
@@ -82,9 +83,9 @@ class CurlImpl {
8283
CurlImpl& operator=(CurlImpl const&) = delete;
8384
CurlImpl& operator=(CurlImpl&&) = default;
8485

85-
void SetHeader(std::string const& header);
86-
void SetHeader(std::pair<std::string, std::string> const& header);
87-
void SetHeaders(RestContext const& context, RestRequest const& request);
86+
void SetHeader(HttpHeader header);
87+
void SetHeaders(
88+
std::unordered_map<std::string, std::vector<std::string>> const& headers);
8889

8990
std::string MakeEscapedString(std::string const& s);
9091

@@ -108,11 +109,20 @@ class CurlImpl {
108109
std::size_t WriteCallback(absl::Span<char> response);
109110
std::size_t HeaderCallback(absl::Span<char> response);
110111

112+
// Traverses the accrued headers added via SetHeader(s), merges the values of
113+
// headers with like keys and writes them via the write_fn.
114+
// The write_fn is provided for testing purposes. When used internally, the
115+
// write_fn writes to libcurl buffers.
116+
void MergeAndWriteHeaders(
117+
std::function<void(HttpHeader const&)> const& write_fn);
118+
111119
private:
112120
class ReadFunctionAbortGuard;
113121
Status MakeRequestImpl(RestContext& context);
114122
StatusOr<std::size_t> ReadImpl(RestContext& context, absl::Span<char> output);
115123

124+
void WriteHeader(std::string const& header);
125+
116126
// Cleanup the CURL handles, leaving them ready for reuse.
117127
void CleanupHandles();
118128
// Perform at least part of the request.
@@ -126,6 +136,7 @@ class CurlImpl {
126136
void OnTransferDone();
127137

128138
std::shared_ptr<CurlHandleFactory> factory_;
139+
std::vector<HttpHeader> pending_request_headers_;
129140
CurlHeaders request_headers_;
130141
CurlHandle handle_;
131142
CurlMulti multi_;

google/cloud/internal/curl_impl_test.cc

Lines changed: 204 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
#include "google/cloud/internal/curl_impl.h"
1616
#include "google/cloud/common_options.h"
1717
#include "google/cloud/rest_options.h"
18-
#include "google/cloud/testing_util/status_matchers.h"
1918
#include <gmock/gmock.h>
2019
#include <vector>
2120

@@ -189,6 +188,210 @@ TEST_F(CurlImplTest, CurlOptInterface) {
189188
absl::make_optional(std::string("interface")));
190189
}
191190

191+
TEST_F(CurlImplTest, MergeAndWriteHeadersEmpty) {
192+
std::vector<std::string> headers_written;
193+
auto write_fn = [&headers_written](HttpHeader const& header) {
194+
headers_written.push_back(std::string(header));
195+
};
196+
auto impl = CurlImpl(std::move(handle_), factory_, {});
197+
impl.MergeAndWriteHeaders(write_fn);
198+
EXPECT_THAT(headers_written, ElementsAre());
199+
}
200+
201+
TEST_F(CurlImplTest, MergeAndWriteHeadersOneEntry) {
202+
std::vector<std::string> headers_written;
203+
auto write_fn = [&headers_written](HttpHeader const& header) {
204+
headers_written.push_back(std::string(header));
205+
};
206+
auto impl = CurlImpl(std::move(handle_), factory_, {});
207+
HttpHeader h("my-key", "my-value");
208+
impl.SetHeader(h);
209+
impl.MergeAndWriteHeaders(write_fn);
210+
EXPECT_THAT(headers_written, ElementsAre(std::string(h)));
211+
}
212+
213+
TEST_F(CurlImplTest, MergeAndWriteHeadersTwoUniqueEntries) {
214+
std::vector<std::string> headers_written;
215+
auto write_fn = [&headers_written](HttpHeader const& header) {
216+
headers_written.push_back(std::string(header));
217+
};
218+
auto impl = CurlImpl(std::move(handle_), factory_, {});
219+
HttpHeader h("my-key", "my-value");
220+
impl.SetHeader(h);
221+
HttpHeader h2("my-key2", "my-value2");
222+
impl.SetHeader(h2);
223+
impl.MergeAndWriteHeaders(write_fn);
224+
EXPECT_THAT(headers_written, ElementsAre(std::string(h), std::string(h2)));
225+
}
226+
227+
TEST_F(CurlImplTest, MergeAndWriteHeadersMoreThanTwoUniqueEntries) {
228+
std::vector<std::string> headers_written;
229+
auto write_fn = [&headers_written](HttpHeader const& header) {
230+
headers_written.push_back(std::string(header));
231+
};
232+
auto impl = CurlImpl(std::move(handle_), factory_, {});
233+
HttpHeader h("my-key", "my-value");
234+
impl.SetHeader(h);
235+
HttpHeader h2("my-key2", "my-value2");
236+
impl.SetHeader(h2);
237+
HttpHeader h3("my-key3", "my-value3");
238+
impl.SetHeader(h3);
239+
impl.MergeAndWriteHeaders(write_fn);
240+
EXPECT_THAT(headers_written,
241+
ElementsAre(std::string(h), std::string(h2), std::string(h3)));
242+
}
243+
244+
TEST_F(CurlImplTest, MergeAndWriteHeadersTwoDuplicateEntries) {
245+
std::vector<std::string> headers_written;
246+
auto write_fn = [&headers_written](HttpHeader const& header) {
247+
headers_written.push_back(std::string(header));
248+
};
249+
250+
auto impl = CurlImpl(std::move(handle_), factory_, {});
251+
HttpHeader h("my-key", "my-value");
252+
impl.SetHeader(h);
253+
HttpHeader h2("my-key", "my-value2");
254+
impl.SetHeader(h2);
255+
impl.MergeAndWriteHeaders(write_fn);
256+
HttpHeader expected("my-key", "my-value,my-value2");
257+
EXPECT_THAT(headers_written, ElementsAre(std::string(expected)));
258+
}
259+
260+
TEST_F(CurlImplTest, MergeAndWriteHeadersMoreThanTwoDuplicateEntries) {
261+
std::vector<std::string> headers_written;
262+
auto write_fn = [&headers_written](HttpHeader const& header) {
263+
headers_written.push_back(std::string(header));
264+
};
265+
266+
auto impl = CurlImpl(std::move(handle_), factory_, {});
267+
HttpHeader h("my-key", "my-value");
268+
impl.SetHeader(h);
269+
HttpHeader h2("my-key", "my-value2");
270+
impl.SetHeader(h2);
271+
HttpHeader h3("my-key", "my-value3");
272+
impl.SetHeader(h3);
273+
impl.MergeAndWriteHeaders(write_fn);
274+
HttpHeader expected("my-key", "my-value,my-value2,my-value3");
275+
EXPECT_THAT(headers_written, ElementsAre(std::string(expected)));
276+
}
277+
278+
TEST_F(CurlImplTest, MergeAndWriteHeadersBeginningDuplicateEntries) {
279+
std::vector<std::string> headers_written;
280+
auto write_fn = [&headers_written](HttpHeader const& header) {
281+
headers_written.push_back(std::string(header));
282+
};
283+
284+
auto impl = CurlImpl(std::move(handle_), factory_, {});
285+
HttpHeader i("i-key", "value");
286+
impl.SetHeader(i);
287+
HttpHeader h("h-key", "my-value");
288+
impl.SetHeader(h);
289+
HttpHeader k("k-key", "my-value");
290+
impl.SetHeader(k);
291+
HttpHeader h2("h-key", "my-value2");
292+
impl.SetHeader(h2);
293+
HttpHeader h3("h-key", "my-value3");
294+
impl.SetHeader(h3);
295+
296+
impl.MergeAndWriteHeaders(write_fn);
297+
HttpHeader i_expected("i-key", "value");
298+
HttpHeader h_expected("h-key", "my-value,my-value2,my-value3");
299+
HttpHeader k_expected("k-key", "my-value");
300+
EXPECT_THAT(headers_written,
301+
ElementsAre(std::string(h_expected), std::string(i_expected),
302+
std::string(k_expected)));
303+
}
304+
305+
TEST_F(CurlImplTest, MergeAndWriteHeadersMiddleDuplicateEntries) {
306+
std::vector<std::string> headers_written;
307+
auto write_fn = [&headers_written](HttpHeader const& header) {
308+
headers_written.push_back(std::string(header));
309+
};
310+
311+
auto impl = CurlImpl(std::move(handle_), factory_, {});
312+
HttpHeader i("i-key", "value");
313+
impl.SetHeader(i);
314+
HttpHeader h("h-key", "my-value");
315+
impl.SetHeader(h);
316+
HttpHeader k("k-key", "my-value");
317+
impl.SetHeader(k);
318+
HttpHeader i2("i-key", "my-value2");
319+
impl.SetHeader(i2);
320+
HttpHeader i3("i-key", "my-value3");
321+
impl.SetHeader(i3);
322+
323+
impl.MergeAndWriteHeaders(write_fn);
324+
HttpHeader i_expected("i-key", "value,my-value2,my-value3");
325+
HttpHeader h_expected("h-key", "my-value");
326+
HttpHeader k_expected("k-key", "my-value");
327+
EXPECT_THAT(headers_written,
328+
ElementsAre(std::string(h_expected), std::string(i_expected),
329+
std::string(k_expected)));
330+
}
331+
332+
TEST_F(CurlImplTest, MergeAndWriteHeadersEndingDuplicateEntries) {
333+
std::vector<std::string> headers_written;
334+
auto write_fn = [&headers_written](HttpHeader const& header) {
335+
headers_written.push_back(std::string(header));
336+
};
337+
338+
auto impl = CurlImpl(std::move(handle_), factory_, {});
339+
HttpHeader i("i-key", "value");
340+
impl.SetHeader(i);
341+
HttpHeader h("h-key", "my-value");
342+
impl.SetHeader(h);
343+
HttpHeader k("k-key", "my-value");
344+
impl.SetHeader(k);
345+
HttpHeader k2("k-key", "my-value2");
346+
impl.SetHeader(k2);
347+
HttpHeader k3("k-key", "my-value3");
348+
impl.SetHeader(k3);
349+
350+
impl.MergeAndWriteHeaders(write_fn);
351+
HttpHeader i_expected("i-key", "value");
352+
HttpHeader h_expected("h-key", "my-value");
353+
HttpHeader k_expected("k-key", "my-value,my-value2,my-value3");
354+
EXPECT_THAT(headers_written,
355+
ElementsAre(std::string(h_expected), std::string(i_expected),
356+
std::string(k_expected)));
357+
}
358+
359+
TEST_F(CurlImplTest, MergeAndWriteHeadersDoNotMergeAuthorization) {
360+
std::vector<std::string> headers_written;
361+
auto write_fn = [&headers_written](HttpHeader const& header) {
362+
headers_written.push_back(std::string(header));
363+
};
364+
365+
auto impl = CurlImpl(std::move(handle_), factory_, {});
366+
HttpHeader auth("Authorization", "Bearer my-token");
367+
impl.SetHeader(auth);
368+
HttpHeader auth2("Authorization", "Bearer my-token");
369+
impl.SetHeader(auth2);
370+
HttpHeader auth3("authorization", "Bearer my-token");
371+
impl.SetHeader(auth3);
372+
impl.MergeAndWriteHeaders(write_fn);
373+
HttpHeader expected("Authorization", "Bearer my-token");
374+
EXPECT_THAT(headers_written, ElementsAre(std::string(expected)));
375+
}
376+
377+
TEST_F(CurlImplTest, MergeAndWriteHeadersDoNotMergeContentLength) {
378+
std::vector<std::string> headers_written;
379+
auto write_fn = [&headers_written](HttpHeader const& header) {
380+
headers_written.push_back(std::string(header));
381+
};
382+
383+
auto impl = CurlImpl(std::move(handle_), factory_, {});
384+
HttpHeader auth("content-length", "42");
385+
impl.SetHeader(auth);
386+
HttpHeader auth2("Content-Length", "43");
387+
impl.SetHeader(auth2);
388+
HttpHeader auth3("content-length", "44");
389+
impl.SetHeader(auth3);
390+
impl.MergeAndWriteHeaders(write_fn);
391+
HttpHeader expected("content-length", "42");
392+
EXPECT_THAT(headers_written, ElementsAre(std::string(expected)));
393+
}
394+
192395
} // namespace
193396
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
194397
} // namespace rest_internal

0 commit comments

Comments
 (0)