|
22 | 22 | import urllib
|
23 | 23 | from http import HTTPStatus
|
24 | 24 | from io import BytesIO
|
25 |
| -from typing import Any, Callable, Dict, Tuple, Union |
| 25 | +from typing import Any, Callable, Dict, Iterator, List, Tuple, Union |
26 | 26 |
|
27 | 27 | import jinja2
|
28 |
| -from canonicaljson import encode_canonical_json, encode_pretty_printed_json |
| 28 | +from canonicaljson import iterencode_canonical_json, iterencode_pretty_printed_json |
| 29 | +from zope.interface import implementer |
29 | 30 |
|
30 |
| -from twisted.internet import defer |
| 31 | +from twisted.internet import defer, interfaces |
31 | 32 | from twisted.python import failure
|
32 | 33 | from twisted.web import resource
|
33 | 34 | from twisted.web.server import NOT_DONE_YET, Request
|
@@ -499,6 +500,78 @@ class RootOptionsRedirectResource(OptionsResource, RootRedirect):
|
499 | 500 | pass
|
500 | 501 |
|
501 | 502 |
|
| 503 | +@implementer(interfaces.IPullProducer) |
| 504 | +class _ByteProducer: |
| 505 | + """ |
| 506 | + Iteratively write bytes to the request. |
| 507 | + """ |
| 508 | + |
| 509 | + # The minimum number of bytes for each chunk. Note that the last chunk will |
| 510 | + # usually be smaller than this. |
| 511 | + min_chunk_size = 1024 |
| 512 | + |
| 513 | + def __init__( |
| 514 | + self, request: Request, iterator: Iterator[bytes], |
| 515 | + ): |
| 516 | + self._request = request |
| 517 | + self._iterator = iterator |
| 518 | + |
| 519 | + def start(self) -> None: |
| 520 | + self._request.registerProducer(self, False) |
| 521 | + |
| 522 | + def _send_data(self, data: List[bytes]) -> None: |
| 523 | + """ |
| 524 | + Send a list of strings as a response to the request. |
| 525 | + """ |
| 526 | + if not data: |
| 527 | + return |
| 528 | + self._request.write(b"".join(data)) |
| 529 | + |
| 530 | + def resumeProducing(self) -> None: |
| 531 | + # We've stopped producing in the meantime (note that this might be |
| 532 | + # re-entrant after calling write). |
| 533 | + if not self._request: |
| 534 | + return |
| 535 | + |
| 536 | + # Get the next chunk and write it to the request. |
| 537 | + # |
| 538 | + # The output of the JSON encoder is coalesced until min_chunk_size is |
| 539 | + # reached. (This is because JSON encoders produce a very small output |
| 540 | + # per iteration.) |
| 541 | + # |
| 542 | + # Note that buffer stores a list of bytes (instead of appending to |
| 543 | + # bytes) to hopefully avoid many allocations. |
| 544 | + buffer = [] |
| 545 | + buffered_bytes = 0 |
| 546 | + while buffered_bytes < self.min_chunk_size: |
| 547 | + try: |
| 548 | + data = next(self._iterator) |
| 549 | + buffer.append(data) |
| 550 | + buffered_bytes += len(data) |
| 551 | + except StopIteration: |
| 552 | + # The entire JSON object has been serialized, write any |
| 553 | + # remaining data, finalize the producer and the request, and |
| 554 | + # clean-up any references. |
| 555 | + self._send_data(buffer) |
| 556 | + self._request.unregisterProducer() |
| 557 | + self._request.finish() |
| 558 | + self.stopProducing() |
| 559 | + return |
| 560 | + |
| 561 | + self._send_data(buffer) |
| 562 | + |
| 563 | + def stopProducing(self) -> None: |
| 564 | + self._request = None |
| 565 | + |
| 566 | + |
| 567 | +def _encode_json_bytes(json_object: Any) -> Iterator[bytes]: |
| 568 | + """ |
| 569 | + Encode an object into JSON. Returns an iterator of bytes. |
| 570 | + """ |
| 571 | + for chunk in json_encoder.iterencode(json_object): |
| 572 | + yield chunk.encode("utf-8") |
| 573 | + |
| 574 | + |
502 | 575 | def respond_with_json(
|
503 | 576 | request: Request,
|
504 | 577 | code: int,
|
@@ -533,15 +606,23 @@ def respond_with_json(
|
533 | 606 | return None
|
534 | 607 |
|
535 | 608 | if pretty_print:
|
536 |
| - json_bytes = encode_pretty_printed_json(json_object) + b"\n" |
| 609 | + encoder = iterencode_pretty_printed_json |
537 | 610 | else:
|
538 | 611 | if canonical_json or synapse.events.USE_FROZEN_DICTS:
|
539 |
| - # canonicaljson already encodes to bytes |
540 |
| - json_bytes = encode_canonical_json(json_object) |
| 612 | + encoder = iterencode_canonical_json |
541 | 613 | else:
|
542 |
| - json_bytes = json_encoder.encode(json_object).encode("utf-8") |
| 614 | + encoder = _encode_json_bytes |
| 615 | + |
| 616 | + request.setResponseCode(code) |
| 617 | + request.setHeader(b"Content-Type", b"application/json") |
| 618 | + request.setHeader(b"Cache-Control", b"no-cache, no-store, must-revalidate") |
543 | 619 |
|
544 |
| - return respond_with_json_bytes(request, code, json_bytes, send_cors=send_cors) |
| 620 | + if send_cors: |
| 621 | + set_cors_headers(request) |
| 622 | + |
| 623 | + producer = _ByteProducer(request, encoder(json_object)) |
| 624 | + producer.start() |
| 625 | + return NOT_DONE_YET |
545 | 626 |
|
546 | 627 |
|
547 | 628 | def respond_with_json_bytes(
|
|
0 commit comments