0

I followed these steps to post a PDF file to Drupal.

  1. Enable JSON API to create at admin/config/services/jsonapi

  2. Enable the media POST resource

    Media /media/{media}/edit: GET, PATCH, DELETE
          /entity/media: POST
    
    Methods: POST, formats: json: authentication: cookie
    
  3. Create an Angular application and embed it in the Drupal page as a block

The Angular application captures the page using jspdf and embeds the image in the PDF. Instead of using an interceptor, I'm getting a token with each request.

this.certificateService.getCsrf().subscribe(token => {
  this.certificateService.uploadMedia(pdf.output('blob'), fileName, token).subscribe({
    complete: () => {
      console.log('posted media');
    }
  })
});

getCsrf() grabs the token as text.

getCsrf(): Observable<string> {
  const headers = new HttpHeaders().set('Content-Type', 'text/plain; charset=utf-8');

  return this.httpClient.get(this.host + '/session/token',{ headers: headers, responseType: 'text'}).pipe(catchError(this.handleError<any>('token')))
}

uploadMedia() tries to post the PDF file to media to Drupal.

uploadMedia(pdf: Blob, name: string, token: string): Observable<any> {
  const formData: FormData = new FormData();
  formData.append('certificate', pdf, name);
  const headers = new HttpHeaders();
  headers.set('Accept', 'application/vnd.api+json');
  headers.set('X-CSRF-Token',token);
  // headers.set('Content-Type','application/octet-stream');
  headers.set('Content-Type','application/hal+json');
  headers.set('Content-Disposition',`file; filename=${name}`);

  return this.httpClient.post(this.host + '/entity/media', formData, {headers: headers})
    .pipe(
      catchError(this.handleError<any>('uploadMedia'))
    );
}

The error I get is 415 Unsupported media type.

415 Unsupported media type

I am using Angular 13 and Drupal 9.

I looked at the Drupal error log; it reports the following exception.

Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException: No route found that matches "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryYAzO5lNTfgLQwQq9" in Drupal\Core\Routing\ContentTypeHeaderMatcher->filter()

Just setting the Content-Type header does not always alter the content type header that is posted. e.g. With both axios and angular http if you post a FormData array you will get a multi-part form even though you the header at the call level is application/octet-stream

Using axios within angular as follows result in 'application/octet-stream':

import Axios from 'axios-observable'; 
Axios.defaults.headers.post['X-CSRF-Token'] = token;
return Axios.post(this.host + '/entity/media', pdf, {
  headers: {
    Accept: `application/json, text/plain, */*',
    Authorization: `Basic ${basic}`,
    Cookie: ${cookie}; XDEBUG_SESSION=13681',
    'Content-Type': 'application/octet-stream',
  }
});

Passing raw PDF data results in 'application/pdf'

const headers = new HttpHeaders();
headers.set('Content-Type', 'application/octet-stream');
return this.httpClient.post(this.host + '/entity/media', pdf, {headers: headers})
  .pipe(
    catchError(this.handleError<any>('uploadMedia'))
);

Reviewing the rest services page admin/config/services/rest, the allowed content format is 'json'

Debugging confirms that symfony is checking this value

Symfony accept JSON

Trying a REST interface i.e. '/jsonapi/media/document/field_media_document/'

Yes there is a 'bin' content type requirement in web/core/modules/file/src/Plugin/rest/resource/FileUploadResource.php which

// Add the content type format access check. This will enforce that all
// incoming requests can only use the 'application/octet-stream'
// Content-Type header.

The /entity/media JSON API request url does need a JSON request as per web/core/lib/Drupal/Core/Routing/ContentTypeHeaderMatcher.php

entity media json request

1 Answer 1

0

I use Angular, not React, but I'm pretty sure this part is wrong:

  // headers.set('Content-Type','application/octet-stream');
  headers.set('Content-Type','application/hal+json');

Files need to be posted as application/octet-stream. It's not a JSON object; it's a file.

Also, you may need to massage the PDF data into something that JSON:API understands. For example, here's the docs on doing this with axios/Node.js.

In my case, for image files, I had to read the file in as a blob (React code, but may provide an example):

// https://stackoverflow.com/a/46568146
function readFileBlob(fileBlob: Blob) {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.onload = () => {
      resolve(reader.result);
    };
    reader.onabort = () => Promise.reject(Error('file reading was aborted'));
    reader.onerror = () => Promise.reject(Error('file reading has failed'));
    reader.readAsArrayBuffer(fileBlob);
  });
}

// fileToPost is a string that contains a blob:
// blob:http://localhost:8100/00507d3e-7f6f-4f48-be80-b55c8bc20dd3
const postAuthFileBlob = async (
  fetchUrl: string,
  fileToPost: string,
  // Fetch might return anything.
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
): Promise<any> => {
  const fileBlob = await fetch(fileToPost).then((res) => res.blob());
  const arrayStr = (await readFileBlob(fileBlob)) as ArrayBuffer;
  return postAuthFileBlobPart2(fetchUrl, arrayStr);
};
3
  • I think you are close. The back-end is complaining that the content-type is multipart/form-data instead of application/octet-stream
    – Interlated
    Mar 6, 2022 at 7:38
  • Just posting the pdf gives application/pdf return this.httpClient.post(this.host + '/entity/media', pdf, {headers: headers}). Still drupal cannot find a match for application/pdf. No route found that matches "Content-Type: application/pdf" in Drupal\Core\Routing\ContentTypeHeaderMatcher->filter() (line 49 of
    – Interlated
    Mar 6, 2022 at 7:43
  • @Interlated As I already stated, you need to post as application/octet-stream. For some reason you commented that out in your code, but you have to post as application/octet-stream if you want to upload any binary files. Mar 6, 2022 at 7:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.