Repository: solid/community-server
Pull request #500 (2021-01-12 14:41:20)

Allow Content-Type: 0 on GET
Fixes https://github.com/solid/community-server/issues/498


Patch:
diff --git a/src/ldp/http/RawBodyParser.ts b/src/ldp/http/RawBodyParser.ts
index 57ac98d44..b3864e63a 100644
--- a/src/ldp/http/RawBodyParser.ts
+++ b/src/ldp/http/RawBodyParser.ts
@@ -14,18 +14,26 @@ export class RawBodyParser extends BodyParser {
   // Note that the only reason this is a union is in case the body is empty.
   // If this check gets moved away from the BodyParsers this union could be removed
   public async handle({ request, metadata }: BodyParserArgs): Promise<Representation | undefined> {
+    const {
+      'content-type': contentType,
+      'content-length': contentLength,
+      'transfer-encoding': transferEncoding,
+    } = request.headers;
+
     // RFC7230, §3.3: The presence of a message body in a request
     // is signaled by a Content-Length or Transfer-Encoding header field.
-    if (!request.headers['content-length'] && !request.headers['transfer-encoding']) {
-      this.logger.debug('HTTP request appears to not have a body, so nothing to parse');
+    // While clients SHOULD NOT use use a Content-Length header on GET,
+    // some still provide a Content-Length of 0 (but without Content-Type).
+    if ((!contentLength || (/^0+$/u.test(contentLength) && !contentType)) && !transferEncoding) {
+      this.logger.debug('HTTP request does not have a body, or its empty body is missing a Content-Type header');
       return;
     }
 
     // While RFC7231 allows treating a body without content type as an octet stream,
     // such an omission likely signals a mistake, so force clients to make this explicit.
-    if (!request.headers['content-type']) {
-      this.logger.warn('A body was passed, but the content length was not specified');
-      throw new BadRequestHttpError('HTTP request body was passed without Content-Type header');
+    if (!contentType) {
+      this.logger.warn('HTTP request has a body, but no Content-Type header');
+      throw new BadRequestHttpError('HTTP request body was passed without a Content-Type header');
     }
 
     return new BasicRepresentation(request, metadata);


Test patch:
diff --git a/test/integration/ServerWithAuth.test.ts b/test/integration/ServerWithAuth.test.ts
index cdcb95b6d..145d995a9 100644
--- a/test/integration/ServerWithAuth.test.ts
+++ b/test/integration/ServerWithAuth.test.ts
@@ -86,4 +86,31 @@ describe('A server with authorization', (): void => {
     );
     expect(response.statusCode).toBe(401);
   });
+
+  // https://github.com/solid/community-server/issues/498
+  it('accepts a GET with Content-Length: 0.', async(): Promise<void> => {
+    await aclHelper.setSimpleAcl({ read: true, write: true, append: true }, 'agent');
+
+    // PUT
+    let requestUrl = new URL('http://test.com/foo/bar');
+    let response: MockResponse<any> = await performRequest(
+      handler,
+      requestUrl,
+      'PUT',
+      { 'content-length': '0', 'content-type': 'text/turtle' },
+      [],
+    );
+    expect(response.statusCode).toBe(205);
+
+    // GET
+    requestUrl = new URL('http://test.com/foo/bar');
+    response = await performRequest(
+      handler,
+      requestUrl,
+      'GET',
+      { 'content-length': '0' },
+      [],
+    );
+    expect(response.statusCode).toBe(200);
+  });
 });
diff --git a/test/unit/ldp/http/RawBodyParser.test.ts b/test/unit/ldp/http/RawBodyParser.test.ts
index 15a236b4a..d6ab85341 100644
--- a/test/unit/ldp/http/RawBodyParser.test.ts
+++ b/test/unit/ldp/http/RawBodyParser.test.ts
@@ -18,27 +18,46 @@ describe('A RawBodyparser', (): void => {
     await expect(bodyParser.canHandle({} as any)).resolves.toBeUndefined();
   });
 
-  it('returns empty output if there was no content length or transfer encoding.', async(): Promise<void> => {
+  it('returns empty output if there is no content length or transfer encoding.', async(): Promise<void> => {
     input.request = streamifyArray([ '' ]) as HttpRequest;
     input.request.headers = {};
     await expect(bodyParser.handle(input)).resolves.toBeUndefined();
   });
 
-  it('errors when a content length was specified without content type.', async(): Promise<void> => {
-    input.request = streamifyArray([ 'abc' ]) as HttpRequest;
+  // https://github.com/solid/community-server/issues/498
+  it('returns empty output if the content length is 0 and there is no content type.', async(): Promise<void> => {
+    input.request = streamifyArray([ '' ]) as HttpRequest;
     input.request.headers = { 'content-length': '0' };
+    await expect(bodyParser.handle(input)).resolves.toBeUndefined();
+  });
+
+  it('errors when a content length is specified without content type.', async(): Promise<void> => {
+    input.request = streamifyArray([ 'abc' ]) as HttpRequest;
+    input.request.headers = { 'content-length': '1' };
     await expect(bodyParser.handle(input)).rejects
-      .toThrow('HTTP request body was passed without Content-Type header');
+      .toThrow('HTTP request body was passed without a Content-Type header');
   });
 
-  it('errors when a transfer encoding was specified without content type.', async(): Promise<void> => {
+  it('errors when a transfer encoding is specified without content type.', async(): Promise<void> => {
     input.request = streamifyArray([ 'abc' ]) as HttpRequest;
     input.request.headers = { 'transfer-encoding': 'chunked' };
     await expect(bodyParser.handle(input)).rejects
-      .toThrow('HTTP request body was passed without Content-Type header');
+      .toThrow('HTTP request body was passed without a Content-Type header');
+  });
+
+  it('returns a Representation if there is empty data.', async(): Promise<void> => {
+    input.request = streamifyArray([]) as HttpRequest;
+    input.request.headers = { 'content-length': '0', 'content-type': 'text/turtle' };
+    const result = (await bodyParser.handle(input))!;
+    expect(result).toEqual({
+      binary: true,
+      data: input.request,
+      metadata: input.metadata,
+    });
+    await expect(arrayifyStream(result.data)).resolves.toEqual([]);
   });
 
-  it('returns a Representation if there was data.', async(): Promise<void> => {
+  it('returns a Representation if there is non-empty data.', async(): Promise<void> => {
     input.request = streamifyArray([ '<http://test.com/s> <http://test.com/p> <http://test.com/o>.' ]) as HttpRequest;
     input.request.headers = { 'transfer-encoding': 'chunked', 'content-type': 'text/turtle' };
     const result = (await bodyParser.handle(input))!;