Repository: encode/httpx

Issue:
aiter_lines() doesn't return full lines that span multiple chunks
<https://gist.github.com/scr-oath/aa76d200222a0409d09a0d6feb1a13e2> shows an example setup using cherry.py as server that just outputs two lines - the json is big enough to be sent in two chunks; httpx aiter_lines() gets confused and sends data from the middle of the json line - seems to skip the starting part - which was most likely sent in a chunk without a newline

### test-httpx.py
```python
import asyncio
import json

import httpx


class TestHttpx:
    def __init__(self):
        pass

    async def __call__(self):
        http_client = httpx.AsyncClient()
        async with http_client.stream(method="GET", url='http://localhost:8080/lines') as response:
            is_message = True
            async for line in response.aiter_lines():
                is_message = not is_message
                if is_message:
                    message = json.loads(line)
                    print(message)


def main():
    test_httpx = TestHttpx()
    asyncio.run(test_httpx())


if __name__ == '__main__':
    main()
```

Comments:

Patch:
diff --git a/httpx/_decoders.py b/httpx/_decoders.py
index 2a2e703..1ea47b0 100644
--- a/httpx/_decoders.py
+++ b/httpx/_decoders.py
@@ -261,7 +261,7 @@ class LineDecoder:
                     text = text[idx + 1 :]
                     break
                 elif next_char is None:
-                    self.buffer = text
+                    self.buffer += text
                     text = ""
                     break
 
diff --git a/requirements.txt b/requirements.txt
index 246a08e..4260706 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -31,4 +31,4 @@ trustme
 uvicorn
 seed-isort-config
 
-attrs>=19.2  # See: https://github.com/encode/httpx/pull/566#issuecomment-559862665
+attrs>=19.3.0  # See: https://github.com/encode/httpx/pull/566#issuecomment-559862665


Test patch:
diff --git a/tests/test_decoders.py b/tests/test_decoders.py
index 9f2fa51..89c545b 100644
--- a/tests/test_decoders.py
+++ b/tests/test_decoders.py
@@ -225,6 +225,15 @@ def test_line_decoder_nl():
     assert decoder.decode("a\n\nb\nc\n") == ["a\n", "\n", "b\n", "c\n"]
     assert decoder.flush() == []
 
+    # Issue #1033
+    decoder = LineDecoder()
+    assert decoder.decode("") == []
+    assert decoder.decode("12345\n") == ["12345\n"]
+    assert decoder.decode("foo ") == []
+    assert decoder.decode("bar ") == []
+    assert decoder.decode("baz\n") == ["foo bar baz\n"]
+    assert decoder.flush() == []
+
 
 def test_line_decoder_cr():
     decoder = LineDecoder()
@@ -237,6 +246,16 @@ def test_line_decoder_cr():
     assert decoder.decode("a\r\rb\rc\r") == ["a\n", "\n", "b\n"]
     assert decoder.flush() == ["c\n"]
 
+    # Issue #1033
+    # TODO: This seems like another bug; fix expectations and results.
+    decoder = LineDecoder()
+    assert decoder.decode("") == []
+    assert decoder.decode("12345\r") == []
+    assert decoder.decode("foo ") == []
+    assert decoder.decode("bar ") == []
+    assert decoder.decode("baz\r") == []
+    assert decoder.flush() == ["12345\rfoo bar baz\n"]
+
 
 def test_line_decoder_crnl():
     decoder = LineDecoder()
@@ -255,6 +274,15 @@ def test_line_decoder_crnl():
     assert decoder.decode("\n\r\nb\r\nc") == ["a\n", "\n", "b\n"]
     assert decoder.flush() == ["c"]
 
+    # Issue #1033
+    decoder = LineDecoder()
+    assert decoder.decode("") == []
+    assert decoder.decode("12345\r\n") == ["12345\n"]
+    assert decoder.decode("foo ") == []
+    assert decoder.decode("bar ") == []
+    assert decoder.decode("baz\r\n") == ["foo bar baz\n"]
+    assert decoder.flush() == []
+
 
 def test_invalid_content_encoding_header():
     headers = [(b"Content-Encoding", b"invalid-header")]