capnproto

FORK: Cap'n Proto serialization/RPC system - core tools and C++ library
git clone https://git.neptards.moe/neptards/capnproto.git
Log | Files | Refs | README | LICENSE

http-over-capnp-test.c++ (17921B)


      1 // Copyright (c) 2019 Cloudflare, Inc. and contributors
      2 // Licensed under the MIT License:
      3 //
      4 // Permission is hereby granted, free of charge, to any person obtaining a copy
      5 // of this software and associated documentation files (the "Software"), to deal
      6 // in the Software without restriction, including without limitation the rights
      7 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
      8 // copies of the Software, and to permit persons to whom the Software is
      9 // furnished to do so, subject to the following conditions:
     10 //
     11 // The above copyright notice and this permission notice shall be included in
     12 // all copies or substantial portions of the Software.
     13 //
     14 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     15 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     16 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     17 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     18 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     19 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
     20 // THE SOFTWARE.
     21 
     22 #include "http-over-capnp.h"
     23 #include <kj/test.h>
     24 
     25 namespace capnp {
     26 namespace {
     27 
     28 KJ_TEST("KJ and RPC HTTP method enums match") {
     29 #define EXPECT_MATCH(METHOD) \
     30   KJ_EXPECT(static_cast<uint>(kj::HttpMethod::METHOD) == \
     31             static_cast<uint>(capnp::HttpMethod::METHOD));
     32 
     33   KJ_HTTP_FOR_EACH_METHOD(EXPECT_MATCH);
     34 #undef EXPECT_MATCH
     35 }
     36 
     37 // =======================================================================================
     38 
     39 kj::Promise<void> expectRead(kj::AsyncInputStream& in, kj::StringPtr expected) {
     40   if (expected.size() == 0) return kj::READY_NOW;
     41 
     42   auto buffer = kj::heapArray<char>(expected.size());
     43 
     44   auto promise = in.tryRead(buffer.begin(), 1, buffer.size());
     45   return promise.then(kj::mvCapture(buffer, [&in,expected](kj::Array<char> buffer, size_t amount) {
     46     if (amount == 0) {
     47       KJ_FAIL_ASSERT("expected data never sent", expected);
     48     }
     49 
     50     auto actual = buffer.slice(0, amount);
     51     if (memcmp(actual.begin(), expected.begin(), actual.size()) != 0) {
     52       KJ_FAIL_ASSERT("data from stream doesn't match expected", expected, actual);
     53     }
     54 
     55     return expectRead(in, expected.slice(amount));
     56   }));
     57 }
     58 
     59 enum Direction {
     60   CLIENT_TO_SERVER,
     61   SERVER_TO_CLIENT
     62 };
     63 
     64 struct TestStep {
     65   Direction direction;
     66   kj::StringPtr send;
     67   kj::StringPtr receive;
     68 
     69   constexpr TestStep(Direction direction, kj::StringPtr send, kj::StringPtr receive)
     70       : direction(direction), send(send), receive(receive) {}
     71   constexpr TestStep(Direction direction, kj::StringPtr data)
     72       : direction(direction), send(data), receive(data) {}
     73 };
     74 
     75 constexpr TestStep TEST_STEPS[] = {
     76   // Test basic request.
     77   {
     78     CLIENT_TO_SERVER,
     79 
     80     "GET / HTTP/1.1\r\n"
     81     "Host: example.com\r\n"
     82     "\r\n"_kj,
     83   },
     84   {
     85     SERVER_TO_CLIENT,
     86 
     87     "HTTP/1.1 200 OK\r\n"
     88     "Content-Length: 3\r\n"
     89     "\r\n"
     90     "foo"_kj
     91   },
     92 
     93   // Try PUT, vary path, vary status
     94   {
     95     CLIENT_TO_SERVER,
     96 
     97     "PUT /foo/bar HTTP/1.1\r\n"
     98     "Content-Length: 5\r\n"
     99     "Host: example.com\r\n"
    100     "\r\n"
    101     "corge"_kj,
    102   },
    103   {
    104     SERVER_TO_CLIENT,
    105 
    106     "HTTP/1.1 403 Unauthorized\r\n"
    107     "Content-Length: 4\r\n"
    108     "\r\n"
    109     "nope"_kj
    110   },
    111 
    112   // HEAD request
    113   {
    114     CLIENT_TO_SERVER,
    115 
    116     "HEAD /foo/bar HTTP/1.1\r\n"
    117     "Host: example.com\r\n"
    118     "\r\n"_kj,
    119   },
    120   {
    121     SERVER_TO_CLIENT,
    122 
    123     "HTTP/1.1 200 OK\r\n"
    124     "Content-Length: 4\r\n"
    125     "\r\n"_kj
    126   },
    127 
    128   // Empty-body response
    129   {
    130     CLIENT_TO_SERVER,
    131 
    132     "GET /foo/bar HTTP/1.1\r\n"
    133     "Host: example.com\r\n"
    134     "\r\n"_kj,
    135   },
    136   {
    137     SERVER_TO_CLIENT,
    138 
    139     "HTTP/1.1 304 Not Modified\r\n"
    140     "Server: foo\r\n"
    141     "\r\n"_kj
    142   },
    143 
    144   // Chonky body
    145   {
    146     CLIENT_TO_SERVER,
    147 
    148     "POST / HTTP/1.1\r\n"
    149     "Transfer-Encoding: chunked\r\n"
    150     "Host: example.com\r\n"
    151     "\r\n"
    152     "3\r\n"
    153     "foo\r\n"
    154     "5\r\n"
    155     "corge\r\n"
    156     "0\r\n"
    157     "\r\n"_kj,
    158   },
    159   {
    160     SERVER_TO_CLIENT,
    161 
    162     "HTTP/1.1 200 OK\r\n"
    163     "Transfer-Encoding: chunked\r\n"
    164     "\r\n"
    165     "6\r\n"
    166     "barbaz\r\n"
    167     "6\r\n"
    168     "garply\r\n"
    169     "0\r\n"
    170     "\r\n"_kj
    171   },
    172 
    173   // Streaming
    174   {
    175     CLIENT_TO_SERVER,
    176 
    177     "POST / HTTP/1.1\r\n"
    178     "Content-Length: 9\r\n"
    179     "Host: example.com\r\n"
    180     "\r\n"_kj,
    181   },
    182   {
    183     CLIENT_TO_SERVER,
    184 
    185     "foo"_kj,
    186   },
    187   {
    188     CLIENT_TO_SERVER,
    189 
    190     "bar"_kj,
    191   },
    192   {
    193     CLIENT_TO_SERVER,
    194 
    195     "baz"_kj,
    196   },
    197   {
    198     SERVER_TO_CLIENT,
    199 
    200     "HTTP/1.1 200 OK\r\n"
    201     "Transfer-Encoding: chunked\r\n"
    202     "\r\n"_kj,
    203   },
    204   {
    205     SERVER_TO_CLIENT,
    206 
    207     "6\r\n"
    208     "barbaz\r\n"_kj,
    209   },
    210   {
    211     SERVER_TO_CLIENT,
    212 
    213     "6\r\n"
    214     "garply\r\n"_kj,
    215   },
    216   {
    217     SERVER_TO_CLIENT,
    218 
    219     "0\r\n"
    220     "\r\n"_kj
    221   },
    222 
    223   // Bidirectional.
    224   {
    225     CLIENT_TO_SERVER,
    226 
    227     "POST / HTTP/1.1\r\n"
    228     "Content-Length: 9\r\n"
    229     "Host: example.com\r\n"
    230     "\r\n"_kj,
    231   },
    232   {
    233     SERVER_TO_CLIENT,
    234 
    235     "HTTP/1.1 200 OK\r\n"
    236     "Transfer-Encoding: chunked\r\n"
    237     "\r\n"_kj,
    238   },
    239   {
    240     CLIENT_TO_SERVER,
    241 
    242     "foo"_kj,
    243   },
    244   {
    245     SERVER_TO_CLIENT,
    246 
    247     "6\r\n"
    248     "barbaz\r\n"_kj,
    249   },
    250   {
    251     CLIENT_TO_SERVER,
    252 
    253     "bar"_kj,
    254   },
    255   {
    256     SERVER_TO_CLIENT,
    257 
    258     "6\r\n"
    259     "garply\r\n"_kj,
    260   },
    261   {
    262     CLIENT_TO_SERVER,
    263 
    264     "baz"_kj,
    265   },
    266   {
    267     SERVER_TO_CLIENT,
    268 
    269     "0\r\n"
    270     "\r\n"_kj
    271   },
    272 
    273   // Test headers being re-ordered by KJ. This isn't necessary behavior, but it does prove that
    274   // we're not testing a pure streaming pass-through...
    275   {
    276     CLIENT_TO_SERVER,
    277 
    278     "GET / HTTP/1.1\r\n"
    279     "Host: example.com\r\n"
    280     "Accept: text/html\r\n"
    281     "Foo-Header: 123\r\n"
    282     "User-Agent: kj\r\n"
    283     "Accept-Language: en\r\n"
    284     "\r\n"_kj,
    285 
    286     "GET / HTTP/1.1\r\n"
    287     "Host: example.com\r\n"
    288     "Accept-Language: en\r\n"
    289     "Accept: text/html\r\n"
    290     "User-Agent: kj\r\n"
    291     "Foo-Header: 123\r\n"
    292     "\r\n"_kj
    293   },
    294   {
    295     SERVER_TO_CLIENT,
    296 
    297     "HTTP/1.1 200 OK\r\n"
    298     "Server: kj\r\n"
    299     "Bar: 321\r\n"
    300     "Content-Length: 3\r\n"
    301     "\r\n"
    302     "foo"_kj,
    303 
    304     "HTTP/1.1 200 OK\r\n"
    305     "Content-Length: 3\r\n"
    306     "Server: kj\r\n"
    307     "Bar: 321\r\n"
    308     "\r\n"
    309     "foo"_kj
    310   },
    311 
    312   // We finish up a request with no response, to test cancellation.
    313   {
    314     CLIENT_TO_SERVER,
    315 
    316     "GET / HTTP/1.1\r\n"
    317     "Host: example.com\r\n"
    318     "\r\n"_kj,
    319   },
    320 };
    321 
    322 class OneConnectNetworkAddress final: public kj::NetworkAddress {
    323 public:
    324   OneConnectNetworkAddress(kj::Own<kj::AsyncIoStream> stream)
    325       : stream(kj::mv(stream)) {}
    326 
    327   kj::Promise<kj::Own<kj::AsyncIoStream>> connect() override {
    328     auto result = KJ_ASSERT_NONNULL(kj::mv(stream));
    329     stream = nullptr;
    330     return kj::mv(result);
    331   }
    332 
    333   kj::Own<kj::ConnectionReceiver> listen() override { KJ_UNIMPLEMENTED("test"); }
    334   kj::Own<kj::NetworkAddress> clone() override { KJ_UNIMPLEMENTED("test"); }
    335   kj::String toString() override { KJ_UNIMPLEMENTED("test"); }
    336 
    337 private:
    338   kj::Maybe<kj::Own<kj::AsyncIoStream>> stream;
    339 };
    340 
    341 void runEndToEndTests(kj::Timer& timer, kj::HttpHeaderTable& headerTable,
    342                       HttpOverCapnpFactory& clientFactory, HttpOverCapnpFactory& serverFactory,
    343                       kj::WaitScope& waitScope) {
    344   auto clientPipe = kj::newTwoWayPipe();
    345   auto serverPipe = kj::newTwoWayPipe();
    346 
    347   OneConnectNetworkAddress oneConnectAddr(kj::mv(serverPipe.ends[0]));
    348 
    349   auto backHttp = kj::newHttpClient(timer, headerTable, oneConnectAddr);
    350   auto backCapnp = serverFactory.kjToCapnp(kj::newHttpService(*backHttp));
    351   auto frontCapnp = clientFactory.capnpToKj(backCapnp);
    352   kj::HttpServer frontKj(timer, headerTable, *frontCapnp);
    353   auto listenTask = frontKj.listenHttp(kj::mv(clientPipe.ends[1]))
    354       .eagerlyEvaluate([](kj::Exception&& e) { KJ_LOG(ERROR, e); });
    355 
    356   for (auto& step: TEST_STEPS) {
    357     KJ_CONTEXT(step.send);
    358 
    359     kj::AsyncOutputStream* out;
    360     kj::AsyncInputStream* in;
    361 
    362     switch (step.direction) {
    363       case CLIENT_TO_SERVER:
    364         out = clientPipe.ends[0];
    365         in = serverPipe.ends[1];
    366         break;
    367       case SERVER_TO_CLIENT:
    368         out = serverPipe.ends[1];
    369         in = clientPipe.ends[0];
    370         break;
    371     }
    372 
    373     auto writePromise = out->write(step.send.begin(), step.send.size());
    374     auto readPromise = expectRead(*in, step.receive);
    375     if (!writePromise.poll(waitScope)) {
    376       if (readPromise.poll(waitScope)) {
    377         readPromise.wait(waitScope);
    378         KJ_FAIL_ASSERT("write hung, read worked fine");
    379       } else {
    380         KJ_FAIL_ASSERT("write and read both hung");
    381       }
    382     }
    383 
    384     writePromise.wait(waitScope);
    385     KJ_ASSERT(readPromise.poll(waitScope), "read hung");
    386     readPromise.wait(waitScope);
    387   }
    388 
    389   // The last test message was a request with no response. If we now close the client end, this
    390   // should propagate all the way through to close the server end!
    391   clientPipe.ends[0] = nullptr;
    392   auto lastRead = serverPipe.ends[1]->readAllText();
    393   KJ_ASSERT(lastRead.poll(waitScope), "last read hung");
    394   KJ_EXPECT(lastRead.wait(waitScope) == nullptr);
    395 }
    396 
    397 KJ_TEST("HTTP-over-Cap'n-Proto E2E, no path shortening") {
    398   kj::EventLoop eventLoop;
    399   kj::WaitScope waitScope(eventLoop);
    400   kj::TimerImpl timer(kj::origin<kj::TimePoint>());
    401 
    402   ByteStreamFactory streamFactory1;
    403   ByteStreamFactory streamFactory2;
    404   kj::HttpHeaderTable::Builder tableBuilder;
    405   HttpOverCapnpFactory factory1(streamFactory1, tableBuilder);
    406   HttpOverCapnpFactory factory2(streamFactory2, tableBuilder);
    407   auto headerTable = tableBuilder.build();
    408 
    409   runEndToEndTests(timer, *headerTable, factory1, factory2, waitScope);
    410 }
    411 
    412 KJ_TEST("HTTP-over-Cap'n-Proto E2E, with path shortening") {
    413   kj::EventLoop eventLoop;
    414   kj::WaitScope waitScope(eventLoop);
    415   kj::TimerImpl timer(kj::origin<kj::TimePoint>());
    416 
    417   ByteStreamFactory streamFactory;
    418   kj::HttpHeaderTable::Builder tableBuilder;
    419   HttpOverCapnpFactory factory(streamFactory, tableBuilder);
    420   auto headerTable = tableBuilder.build();
    421 
    422   runEndToEndTests(timer, *headerTable, factory, factory, waitScope);
    423 }
    424 
    425 KJ_TEST("HTTP-over-Cap'n-Proto 205 bug with HttpClientAdapter") {
    426   // Test that a 205 with a hanging body doesn't prevent headers from being delivered. (This was
    427   // a bug at one point. See, 205 responses are supposed to have empty bodies. But they must
    428   // explicitly indicate an empty body. http-over-capnp, though, *assumed* an empty body when it
    429   // saw a 205. But, on the client side, when HttpClientAdapter sees an empty body, it blocks
    430   // delivery of the *headers* until the service promise resolves, in order to avoid prematurely
    431   // cancelling the service. But on the server side, the service method is left hanging because
    432   // it's waiting for the 205 to actually produce its empty body. If that didn't make any sense,
    433   // consider yourself lucky.)
    434 
    435   kj::EventLoop eventLoop;
    436   kj::WaitScope waitScope(eventLoop);
    437   kj::TimerImpl timer(kj::origin<kj::TimePoint>());
    438 
    439   ByteStreamFactory streamFactory;
    440   kj::HttpHeaderTable::Builder tableBuilder;
    441   HttpOverCapnpFactory factory(streamFactory, tableBuilder);
    442   auto headerTable = tableBuilder.build();
    443 
    444   auto pipe = kj::newTwoWayPipe();
    445 
    446   OneConnectNetworkAddress oneConnectAddr(kj::mv(pipe.ends[0]));
    447 
    448   auto backHttp = kj::newHttpClient(timer, *headerTable, oneConnectAddr);
    449   auto backCapnp = factory.kjToCapnp(kj::newHttpService(*backHttp));
    450   auto frontCapnp = factory.capnpToKj(backCapnp);
    451 
    452   auto frontClient = kj::newHttpClient(*frontCapnp);
    453 
    454   auto req = frontClient->request(kj::HttpMethod::GET, "/", kj::HttpHeaders(*headerTable));
    455 
    456   {
    457     auto readPromise = expectRead(*pipe.ends[1], "GET / HTTP/1.1\r\n\r\n");
    458     KJ_ASSERT(readPromise.poll(waitScope));
    459     readPromise.wait(waitScope);
    460   }
    461 
    462   KJ_EXPECT(!req.response.poll(waitScope));
    463 
    464   {
    465     // A 205 response with no content-length or transfer-encoding is terminated by EOF (but also
    466     // the body is required to be empty). We don't send the EOF yet, just the response line and
    467     // empty headers.
    468     kj::StringPtr resp = "HTTP/1.1 205 Reset Content\r\n\r\n";
    469     pipe.ends[1]->write(resp.begin(), resp.size()).wait(waitScope);
    470   }
    471 
    472   // On the client end, we should get a response now!
    473   KJ_ASSERT(req.response.poll(waitScope));
    474 
    475   auto resp = req.response.wait(waitScope);
    476   KJ_EXPECT(resp.statusCode == 205);
    477 
    478   // But the body is still blocked.
    479   auto promise = resp.body->readAllText();
    480   KJ_EXPECT(!promise.poll(waitScope));
    481 
    482   // OK now send the EOF it's waiting for.
    483   pipe.ends[1]->shutdownWrite();
    484 
    485   // And now the body is unblocked.
    486   KJ_ASSERT(promise.poll(waitScope));
    487   KJ_EXPECT(promise.wait(waitScope) == "");
    488 }
    489 
    490 // =======================================================================================
    491 
    492 class WebSocketAccepter final: public kj::HttpService {
    493 public:
    494   WebSocketAccepter(kj::HttpHeaderTable& headerTable,
    495                     kj::Own<kj::PromiseFulfiller<kj::Own<kj::WebSocket>>> fulfiller,
    496                     kj::Promise<void> done)
    497       : headerTable(headerTable), fulfiller(kj::mv(fulfiller)), done(kj::mv(done)) {}
    498 
    499   kj::Promise<void> request(
    500       kj::HttpMethod method, kj::StringPtr url, const kj::HttpHeaders& headers,
    501       kj::AsyncInputStream& requestBody, Response& response) {
    502     kj::HttpHeaders respHeaders(headerTable);
    503     respHeaders.add("X-Foo", "bar");
    504     fulfiller->fulfill(response.acceptWebSocket(respHeaders));
    505     return kj::mv(done);
    506   }
    507 
    508 private:
    509   kj::HttpHeaderTable& headerTable;
    510   kj::Own<kj::PromiseFulfiller<kj::Own<kj::WebSocket>>> fulfiller;
    511   kj::Promise<void> done;
    512 };
    513 
    514 void runWebSocketTests(kj::HttpHeaderTable& headerTable,
    515                        HttpOverCapnpFactory& clientFactory, HttpOverCapnpFactory& serverFactory,
    516                        kj::WaitScope& waitScope) {
    517   // We take a different approach here, because writing out raw WebSocket frames is a pain.
    518   // It's easier to test WebSockets at the KJ API level.
    519 
    520   auto wsPaf = kj::newPromiseAndFulfiller<kj::Own<kj::WebSocket>>();
    521   auto donePaf = kj::newPromiseAndFulfiller<void>();
    522 
    523   auto back = serverFactory.kjToCapnp(kj::heap<WebSocketAccepter>(
    524     headerTable, kj::mv(wsPaf.fulfiller), kj::mv(donePaf.promise)));
    525   auto front = clientFactory.capnpToKj(back);
    526   auto client = kj::newHttpClient(*front);
    527 
    528   auto resp = client->openWebSocket("/ws", kj::HttpHeaders(headerTable)).wait(waitScope);
    529   KJ_ASSERT(resp.webSocketOrBody.is<kj::Own<kj::WebSocket>>());
    530 
    531   auto clientWs = kj::mv(resp.webSocketOrBody.get<kj::Own<kj::WebSocket>>());
    532   auto serverWs = wsPaf.promise.wait(waitScope);
    533 
    534   {
    535     auto promise = clientWs->send("foo"_kj);
    536     auto message = serverWs->receive().wait(waitScope);
    537     promise.wait(waitScope);
    538     KJ_ASSERT(message.is<kj::String>());
    539     KJ_EXPECT(message.get<kj::String>() == "foo");
    540   }
    541 
    542   {
    543     auto promise = serverWs->send("bar"_kj.asBytes());
    544     auto message = clientWs->receive().wait(waitScope);
    545     promise.wait(waitScope);
    546     KJ_ASSERT(message.is<kj::Array<kj::byte>>());
    547     KJ_EXPECT(kj::str(message.get<kj::Array<kj::byte>>().asChars()) == "bar");
    548   }
    549 
    550   {
    551     auto promise = clientWs->close(1234, "baz"_kj);
    552     auto message = serverWs->receive().wait(waitScope);
    553     promise.wait(waitScope);
    554     KJ_ASSERT(message.is<kj::WebSocket::Close>());
    555     KJ_EXPECT(message.get<kj::WebSocket::Close>().code == 1234);
    556     KJ_EXPECT(message.get<kj::WebSocket::Close>().reason == "baz");
    557   }
    558 
    559   {
    560     auto promise = serverWs->disconnect();
    561     auto receivePromise = clientWs->receive();
    562     KJ_EXPECT(receivePromise.poll(waitScope));
    563     KJ_EXPECT_THROW(DISCONNECTED, receivePromise.wait(waitScope));
    564     promise.wait(waitScope);
    565   }
    566 }
    567 
    568 KJ_TEST("HTTP-over-Cap'n Proto WebSocket, no path shortening") {
    569   kj::EventLoop eventLoop;
    570   kj::WaitScope waitScope(eventLoop);
    571 
    572   ByteStreamFactory streamFactory1;
    573   ByteStreamFactory streamFactory2;
    574   kj::HttpHeaderTable::Builder tableBuilder;
    575   HttpOverCapnpFactory factory1(streamFactory1, tableBuilder);
    576   HttpOverCapnpFactory factory2(streamFactory2, tableBuilder);
    577   auto headerTable = tableBuilder.build();
    578 
    579   runWebSocketTests(*headerTable, factory1, factory2, waitScope);
    580 }
    581 
    582 KJ_TEST("HTTP-over-Cap'n Proto WebSocket, with path shortening") {
    583   kj::EventLoop eventLoop;
    584   kj::WaitScope waitScope(eventLoop);
    585 
    586   ByteStreamFactory streamFactory;
    587   kj::HttpHeaderTable::Builder tableBuilder;
    588   HttpOverCapnpFactory factory(streamFactory, tableBuilder);
    589   auto headerTable = tableBuilder.build();
    590 
    591   runWebSocketTests(*headerTable, factory, factory, waitScope);
    592 }
    593 
    594 // =======================================================================================
    595 // bug fixes
    596 
    597 class HangingHttpService final: public kj::HttpService {
    598 public:
    599   HangingHttpService(bool& called, bool& destroyed)
    600       : called(called), destroyed(destroyed) {}
    601   ~HangingHttpService() noexcept(false) {
    602     destroyed = true;
    603   }
    604 
    605   kj::Promise<void> request(
    606       kj::HttpMethod method, kj::StringPtr url, const kj::HttpHeaders& headers,
    607       kj::AsyncInputStream& requestBody, Response& response) {
    608     called = true;
    609     return kj::NEVER_DONE;
    610   }
    611 
    612 private:
    613   bool& called;
    614   bool& destroyed;
    615 };
    616 
    617 KJ_TEST("HttpService isn't destroyed while call outstanding") {
    618   kj::EventLoop eventLoop;
    619   kj::WaitScope waitScope(eventLoop);
    620 
    621   ByteStreamFactory streamFactory;
    622   kj::HttpHeaderTable::Builder tableBuilder;
    623   HttpOverCapnpFactory factory(streamFactory, tableBuilder);
    624   auto headerTable = tableBuilder.build();
    625 
    626   bool called = false;
    627   bool destroyed = false;
    628   auto service = factory.kjToCapnp(kj::heap<HangingHttpService>(called, destroyed));
    629 
    630   KJ_EXPECT(!called);
    631   KJ_EXPECT(!destroyed);
    632 
    633   auto req = service.startRequestRequest();
    634   auto httpReq = req.initRequest();
    635   httpReq.setMethod(capnp::HttpMethod::GET);
    636   httpReq.setUrl("/");
    637   auto serverContext = req.send().wait(waitScope).getContext();
    638   service = nullptr;
    639 
    640   auto promise = serverContext.whenResolved();
    641   KJ_EXPECT(!promise.poll(waitScope));
    642 
    643   KJ_EXPECT(called);
    644   KJ_EXPECT(!destroyed);
    645 }
    646 
    647 }  // namespace
    648 }  // namespace capnp