Skip to content

Commit 765f2db

Browse files
authored
Merge pull request arduino-libraries#21 from sandeepmistry/chunked-response-body-support
Add support for chunked response bodies
2 parents 448a152 + 0dd9f42 commit 765f2db

File tree

4 files changed

+162
-7
lines changed

4 files changed

+162
-7
lines changed

examples/node_test_server/getPostPutDelete.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,51 @@ function serverStart() {
2121
console.log('Server listening on port '+ port);
2222
}
2323

24+
app.get('/chunked', function(request, response) {
25+
response.write('\n');
26+
response.write(' `:;;;,` .:;;:. \n');
27+
response.write(' .;;;;;;;;;;;` :;;;;;;;;;;: TM \n');
28+
response.write(' `;;;;;;;;;;;;;;;` :;;;;;;;;;;;;;;; \n');
29+
response.write(' :;;;;;;;;;;;;;;;;;; `;;;;;;;;;;;;;;;;;; \n');
30+
response.write(' ;;;;;;;;;;;;;;;;;;;;; .;;;;;;;;;;;;;;;;;;;; \n');
31+
response.write(' ;;;;;;;;:` `;;;;;;;;; ,;;;;;;;;.` .;;;;;;;; \n');
32+
response.write(' .;;;;;;, :;;;;;;; .;;;;;;; ;;;;;;; \n');
33+
response.write(' ;;;;;; ;;;;;;; ;;;;;;, ;;;;;;. \n');
34+
response.write(' ,;;;;; ;;;;;;.;;;;;;` ;;;;;; \n');
35+
response.write(' ;;;;;. ;;;;;;;;;;;` ``` ;;;;;`\n');
36+
response.write(' ;;;;; ;;;;;;;;;, ;;; .;;;;;\n');
37+
response.write('`;;;;: `;;;;;;;; ;;; ;;;;;\n');
38+
response.write(',;;;;` `,,,,,,,, ;;;;;;; .,,;;;,,, ;;;;;\n');
39+
response.write(':;;;;` .;;;;;;;; ;;;;;, :;;;;;;;; ;;;;;\n');
40+
response.write(':;;;;` .;;;;;;;; `;;;;;; :;;;;;;;; ;;;;;\n');
41+
response.write('.;;;;. ;;;;;;;. ;;; ;;;;;\n');
42+
response.write(' ;;;;; ;;;;;;;;; ;;; ;;;;;\n');
43+
response.write(' ;;;;; .;;;;;;;;;; ;;; ;;;;;,\n');
44+
response.write(' ;;;;;; `;;;;;;;;;;;; ;;;;; \n');
45+
response.write(' `;;;;;, .;;;;;; ;;;;;;; ;;;;;; \n');
46+
response.write(' ;;;;;;: :;;;;;;. ;;;;;;; ;;;;;; \n');
47+
response.write(' ;;;;;;;` .;;;;;;;, ;;;;;;;; ;;;;;;;: \n');
48+
response.write(' ;;;;;;;;;:,:;;;;;;;;;: ;;;;;;;;;;:,;;;;;;;;;; \n');
49+
response.write(' `;;;;;;;;;;;;;;;;;;;. ;;;;;;;;;;;;;;;;;;;; \n');
50+
response.write(' ;;;;;;;;;;;;;;;;; :;;;;;;;;;;;;;;;;: \n');
51+
response.write(' ,;;;;;;;;;;;;;, ;;;;;;;;;;;;;; \n');
52+
response.write(' .;;;;;;;;;` ,;;;;;;;;: \n');
53+
response.write(' \n');
54+
response.write(' \n');
55+
response.write(' \n');
56+
response.write(' \n');
57+
response.write(' ;;; ;;;;;` ;;;;: .;; ;; ,;;;;;, ;;. `;, ;;;; \n');
58+
response.write(' ;;; ;;:;;; ;;;;;; .;; ;; ,;;;;;: ;;; `;, ;;;:;; \n');
59+
response.write(' ,;:; ;; ;; ;; ;; .;; ;; ,;, ;;;,`;, ;; ;; \n');
60+
response.write(' ;; ;: ;; ;; ;; ;; .;; ;; ,;, ;;;;`;, ;; ;;. \n');
61+
response.write(' ;: ;; ;;;;;: ;; ;; .;; ;; ,;, ;;`;;;, ;; ;;` \n');
62+
response.write(' ,;;;;; ;;`;; ;; ;; .;; ;; ,;, ;; ;;;, ;; ;; \n');
63+
response.write(' ;; ,;, ;; .;; ;;;;;: ;;;;;: ,;;;;;: ;; ;;, ;;;;;; \n');
64+
response.write(' ;; ;; ;; ;;` ;;;;. `;;;: ,;;;;;, ;; ;;, ;;;; \n');
65+
response.write('\n');
66+
response.end();
67+
});
68+
2469
// this is the POST handler:
2570
app.all('/*', function (request, response) {
2671
console.log('Got a ' + request.method + ' request');

keywords.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ endOfHeadersReached KEYWORD2
3131
endOfBodyReached KEYWORD2
3232
completed KEYWORD2
3333
contentLength KEYWORD2
34+
isResponseChunked KEYWORD2
3435
connectionKeepAlive KEYWORD2
3536
noDefaultRequestHeaders KEYWORD2
3637
headerAvailable KEYWORD2

src/HttpClient.cpp

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
// Initialize constants
99
const char* HttpClient::kUserAgent = "Arduino/2.2.0";
1010
const char* HttpClient::kContentLengthPrefix = HTTP_HEADER_CONTENT_LENGTH ": ";
11+
const char* HttpClient::kTransferEncodingChunked = HTTP_HEADER_TRANSFER_ENCODING ": " HTTP_HEADER_VALUE_CHUNKED;
1112

1213
HttpClient::HttpClient(Client& aClient, const char* aServerName, uint16_t aServerPort)
1314
: iClient(&aClient), iServerName(aServerName), iServerAddress(), iServerPort(aServerPort),
@@ -35,6 +36,9 @@ void HttpClient::resetState()
3536
iContentLength = kNoContentLengthHeader;
3637
iBodyLengthConsumed = 0;
3738
iContentLengthPtr = kContentLengthPrefix;
39+
iTransferEncodingChunkedPtr = kTransferEncodingChunked;
40+
iIsChunked = false;
41+
iChunkLength = 0;
3842
iHttpResponseTimeout = kHttpResponseTimeout;
3943
}
4044

@@ -62,7 +66,7 @@ void HttpClient::beginRequest()
6266
int HttpClient::startRequest(const char* aURLPath, const char* aHttpMethod,
6367
const char* aContentType, int aContentLength, const byte aBody[])
6468
{
65-
if (iState == eReadingBody)
69+
if (iState == eReadingBody || iState == eReadingChunkLength || iState == eReadingBodyChunk)
6670
{
6771
flushClientRx();
6872

@@ -533,6 +537,11 @@ int HttpClient::skipResponseHeaders()
533537
}
534538
}
535539

540+
bool HttpClient::endOfHeadersReached()
541+
{
542+
return (iState == eReadingBody || iState == eReadingChunkLength || iState == eReadingBodyChunk);
543+
};
544+
536545
int HttpClient::contentLength()
537546
{
538547
// skip the response headers, if they haven't been read already
@@ -595,8 +604,62 @@ bool HttpClient::endOfBodyReached()
595604
return false;
596605
}
597606

607+
int HttpClient::available()
608+
{
609+
if (iState == eReadingChunkLength)
610+
{
611+
while (iClient->available())
612+
{
613+
char c = iClient->read();
614+
615+
if (c == '\n')
616+
{
617+
iState = eReadingBodyChunk;
618+
break;
619+
}
620+
else if (c == '\r')
621+
{
622+
// no-op
623+
}
624+
else if (isHexadecimalDigit(c))
625+
{
626+
char digit[2] = {c, '\0'};
627+
628+
iChunkLength = (iChunkLength * 16) + strtol(digit, NULL, 16);
629+
}
630+
}
631+
}
632+
633+
if (iState == eReadingBodyChunk && iChunkLength == 0)
634+
{
635+
iState = eReadingChunkLength;
636+
}
637+
638+
if (iState == eReadingChunkLength)
639+
{
640+
return 0;
641+
}
642+
643+
int clientAvailable = iClient->available();
644+
645+
if (iState == eReadingBodyChunk)
646+
{
647+
return min(clientAvailable, iChunkLength);
648+
}
649+
else
650+
{
651+
return clientAvailable;
652+
}
653+
}
654+
655+
598656
int HttpClient::read()
599657
{
658+
if (iIsChunked && !available())
659+
{
660+
return -1;
661+
}
662+
600663
int ret = iClient->read();
601664
if (ret >= 0)
602665
{
@@ -606,6 +669,16 @@ int HttpClient::read()
606669
// So keep track of how many bytes are left
607670
iBodyLengthConsumed++;
608671
}
672+
673+
if (iState == eReadingBodyChunk)
674+
{
675+
iChunkLength--;
676+
677+
if (iChunkLength == 0)
678+
{
679+
iState = eReadingChunkLength;
680+
}
681+
}
609682
}
610683
return ret;
611684
}
@@ -719,15 +792,26 @@ int HttpClient::readHeader()
719792
iBodyLengthConsumed = 0;
720793
}
721794
}
722-
else if ((iContentLengthPtr == kContentLengthPrefix) && (c == '\r'))
795+
else if (*iTransferEncodingChunkedPtr == c)
796+
{
797+
// This character matches, just move along
798+
iTransferEncodingChunkedPtr++;
799+
if (*iTransferEncodingChunkedPtr == '\0')
800+
{
801+
// We've reached the end of the Transfer Encoding: chunked header
802+
iIsChunked = true;
803+
iState = eSkipToEndOfHeader;
804+
}
805+
}
806+
else if (((iContentLengthPtr == kContentLengthPrefix) && (iTransferEncodingChunkedPtr == kTransferEncodingChunked)) && (c == '\r'))
723807
{
724808
// We've found a '\r' at the start of a line, so this is probably
725809
// the end of the headers
726810
iState = eLineStartingCRFound;
727811
}
728812
else
729813
{
730-
// This isn't the Content-Length header, skip to the end of the line
814+
// This isn't the Content-Length or Transfer Encoding chunked header, skip to the end of the line
731815
iState = eSkipToEndOfHeader;
732816
}
733817
break;
@@ -747,7 +831,15 @@ int HttpClient::readHeader()
747831
case eLineStartingCRFound:
748832
if (c == '\n')
749833
{
750-
iState = eReadingBody;
834+
if (iIsChunked)
835+
{
836+
iState = eReadingChunkLength;
837+
iChunkLength = 0;
838+
}
839+
else
840+
{
841+
iState = eReadingBody;
842+
}
751843
}
752844
break;
753845
default:
@@ -760,6 +852,7 @@ int HttpClient::readHeader()
760852
// We've got to the end of this line, start processing again
761853
iState = eStatusCodeRead;
762854
iContentLengthPtr = kContentLengthPrefix;
855+
iTransferEncodingChunkedPtr = kTransferEncodingChunked;
763856
}
764857
// And return the character read to whoever wants it
765858
return c;

src/HttpClient.h

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ static const int HTTP_ERROR_INVALID_RESPONSE =-4;
3434
#define HTTP_HEADER_CONTENT_LENGTH "Content-Length"
3535
#define HTTP_HEADER_CONTENT_TYPE "Content-Type"
3636
#define HTTP_HEADER_CONNECTION "Connection"
37+
#define HTTP_HEADER_TRANSFER_ENCODING "Transfer-Encoding"
3738
#define HTTP_HEADER_USER_AGENT "User-Agent"
39+
#define HTTP_HEADER_VALUE_CHUNKED "chunked"
3840

3941
class HttpClient : public Client
4042
{
@@ -254,7 +256,7 @@ class HttpClient : public Client
254256
/** Test whether all of the response headers have been consumed.
255257
@return true if we are now processing the response body, else false
256258
*/
257-
bool endOfHeadersReached() { return (iState == eReadingBody); };
259+
bool endOfHeadersReached();
258260

259261
/** Test whether the end of the body has been reached.
260262
Only works if the Content-Length header was returned by the server
@@ -272,6 +274,11 @@ class HttpClient : public Client
272274
*/
273275
int contentLength();
274276

277+
/** Returns if the response body is chunked
278+
@return true if response body is chunked, false otherwise
279+
*/
280+
int isResponseChunked() { return iIsChunked; }
281+
275282
/** Return the response body as a String
276283
Also skips response headers if they have not been read already
277284
MUST be called after responseStatusCode()
@@ -293,7 +300,7 @@ class HttpClient : public Client
293300
virtual size_t write(uint8_t aByte) { if (iState < eRequestSent) { finishHeaders(); }; return iClient-> write(aByte); };
294301
virtual size_t write(const uint8_t *aBuffer, size_t aSize) { if (iState < eRequestSent) { finishHeaders(); }; return iClient->write(aBuffer, aSize); };
295302
// Inherited from Stream
296-
virtual int available() { return iClient->available(); };
303+
virtual int available();
297304
/** Read the next byte from the server.
298305
@return Byte read or -1 if there are no bytes available.
299306
*/
@@ -339,6 +346,7 @@ class HttpClient : public Client
339346
// processing)
340347
static const int kHttpResponseTimeout = 30*1000;
341348
static const char* kContentLengthPrefix;
349+
static const char* kTransferEncodingChunked;
342350
typedef enum {
343351
eIdle,
344352
eRequestStarted,
@@ -348,7 +356,9 @@ class HttpClient : public Client
348356
eReadingContentLength,
349357
eSkipToEndOfHeader,
350358
eLineStartingCRFound,
351-
eReadingBody
359+
eReadingBody,
360+
eReadingChunkLength,
361+
eReadingBodyChunk
352362
} tHttpState;
353363
// Client we're using
354364
Client* iClient;
@@ -367,6 +377,12 @@ class HttpClient : public Client
367377
int iBodyLengthConsumed;
368378
// How far through a Content-Length header prefix we are
369379
const char* iContentLengthPtr;
380+
// How far through a Transfer-Encoding chunked header we are
381+
const char* iTransferEncodingChunkedPtr;
382+
// Stores if the response body is chunked
383+
bool iIsChunked;
384+
// Stores the value of the current chunk length, if present
385+
int iChunkLength;
370386
uint32_t iHttpResponseTimeout;
371387
bool iConnectionClose;
372388
bool iSendDefaultRequestHeaders;

0 commit comments

Comments
 (0)