Answers.Body: <p>There are two distinct problems here, the first with the formatting of your POST request body and the second with the location / routing of your contoller. You were getting the first error because your POST request contained invalid JSON, which means Yii2 returns an error before the request even reaches your controller. When you simplified your request for testing purposes, you probably removed that error, so now the problem is that Yii2 can't reach your controller.</p>
  <h2>Fixing the POST request</h2>
  <p>This is only a minor error – you have the <code>Content-Type: application/json</code> header in your request, but you're not actually passing JSON.</p>
  <pre><code>let data = {};
  data[csrfTokenName] = csrfTokenValue;
  data[&quot;selection&quot;] = pageSelection;
  
  fetch(target, {
    headers: {
      &quot;Content-Type&quot;: &quot;application/json&quot;,
      &quot;Accept&quot;: &quot;application/json, text-plain&quot;,
      &quot;CRAFT_CSRF_TOKEN&quot;: window.csrfTokenValue,
    },
    method: 'POST',
    mode: 'no-cors',
    body: data
  })
  </code></pre>
  <p>Note that <code>data</code> is not a JSON string, but an object with two properties. Only the <code>pageSelection</code> property contains a JSON string. <code>fetch</code> doesn't automatically stringify (encode) data, so when you give it an object, the default <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString" rel="nofollow noreferrer">Object.prototype.toString</a> method is used to convert it for the request. In your case, this will return <code>[object Object]</code>, which is what the server receives. This isn't valid JSON, so you get an error.</p>
  <p>The solution is to stringify your object:</p>
  <pre class="lang-javascript prettyprint-override"><code>body: JSON.stringify(data),
  </code></pre>
  <p>You don't want to encode the URLs to JSON twice, so make sure to remove this line and instead add the array with URls directly to the <code>data</code> object.</p>
  <pre class="lang-javascript prettyprint-override"><code>const pageSelection = JSON.stringify(ar);
  </code></pre>
  <hr />
  <p>To debug errors like this, using the network tab in your browser console is essential. A quick test will show you the problem. In the console, try this code:</p>
  <pre class="lang-javascript prettyprint-override"><code>fetch('/test', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: { foo: 'bar'}
  })
  </code></pre>
  <p>Now check the network tab, click on the fetch request and check the request tab for that request. It will show you that the body isn't a JSON string, but <code>[object Object]</code>.</p>
  <h2>Fixing the controller routing</h2>
  <p>The correct setup depends on what you're trying to build – a plugin that can be installed by others using Composer, or a module that is specific to a single site and included in the repository of the site?</p>
  <ul>
  <li>For a plugin: See <a href="https://craftcms.com/docs/3.x/extend/plugin-guide.html" rel="nofollow noreferrer">How to build a plugin</a>. Make sure to set up your plugin folder according to this guide, and include it with composer using a local path in a test site (as specified in the guide). In the plugin factory, make sure you selected <em>Craft CMS Plugin version 3.x</em>.</li>
  <li>For a site-specific module: See <a href="https://craftcms.com/docs/3.x/extend/module-guide.html" rel="nofollow noreferrer">How to build a module</a>. Your module files go in the <code>modules/</code> directory and you have to adjust the <code>autoload</code> information in the <code>composer.json</code> of your site so it knows to load modules from that folder. In the plugin factory, make sure you selected <em>Craft CMS Module version 3.x</em>.</li>
  </ul>
  <p>If you're having trouble, I recommend <strong>not</strong> to use the plugin factory. Instead, start from scratch and go through the guide step-by-step. The plugin factory is really a power-user tool and includes more than you need, in particular if you're just starting out.</p>
  
Answers.ContentLicense: CC BY-SA 4.0
Answers.CreationDate: 2021-09-29T10:00:38.900Z
Answers.LastActivityDate: 2021-09-29T10:00:38.900Z
Answers.UserDisplayName: MoritzLost
Body: <p>I am currently trying to write my 1st Craft plugin.</p>
  <p>The goal (simplified):
  Backend users select a bunch of pages in the CP, and create a PDF. The result is an annual report that's optimized for print (so each year = 1 site / group, and each Craft page = 1 PDF &quot;chapter&quot;).</p>
  <p>I've used plugin factory for scaffolding, and have the two main pieces together:</p>
  <p>PHP: get URL-list from outside, fetch pages via cURL, extract  content via xPath, re-arrange HTML-headings (h1 becomes h2 etc.), create a table of contents and finally store the PDF somewhere (mPDF library was used).</p>
  <p>Craft CP: Create a sortable pages list with sortable.js, and a button that POSTs the JSON.stringify list with fetch to my plugin controller action.</p>
  <p>No matter what I do, the controller responds with server error 500.
  I've tried cors or no-cors, fiddled around with Apache headers (allow-origin et al), even used a simple old-school form instead of JS, but I was never able to get an HTTP result code 200 instead of 500.</p>
  <p>I have tried it locally first, then remote. What I'm trying to do is certainly not rocket science, I guess I either miss something that's not covered in the Craft docs, or have some server settings wrong (or maybe it's documented, but rather kinda &quot;hidden&quot; somewhere I didn't look yet).</p>
  <p>tl;dr: Posting data from my plugin CP to the plugin's controller action ends up with server error 500, and I'm stuck here. That's the last stumbling block to get my plugin finished, at least for a rough demo / MVP.</p>
  <p>JS inside my custom plugin templates/index.twig:</p>
  <pre><code>button.addEventListener('click', event =&gt; {
    const newList = el.children;
    let ar = [];
    Array.from(newList).forEach((item) =&gt; {
      let url = item.querySelector('a').attributes.href.value;
      ar.push(url);
    })
  
    const pageSelection = JSON.stringify(ar);
    const target = '/actions/pdf-creator/default/get-page-selection';
  
    window.csrfTokenName = &quot;{{ craft.app.config.general.csrfTokenName|e('js') }}&quot;;
    window.csrfTokenValue = &quot;{{ craft.app.request.csrfToken|e('js') }}&quot;;
  
    let data = {};
    data[csrfTokenName] = csrfTokenValue;
    data[&quot;selection&quot;] = pageSelection;
  
    fetch(target, {
      headers: {
        &quot;Content-Type&quot;: &quot;application/json&quot;,
        &quot;Accept&quot;: &quot;application/json, text-plain&quot;,
        &quot;CRAFT_CSRF_TOKEN&quot;: window.csrfTokenValue,
      },
      method: 'POST',
      mode: 'no-cors',
      body: data
    })
      .then(response =&gt; getHeaders(response))
      .then(response =&gt; console.log(response))
  });
  
  </code></pre>
  <p>My controller action: (inside myplugin/src/controllers/DefaultController.php)</p>
  <pre><code>public function actionGetPageSelection() {
      $this-&gt;requirePostRequest();
      $post = Craft::$app-&gt;getRequest();
      //    $rawBody = Craft::$app-&gt;getRequest()-&gt;getRawBody(); // doesn't work either
      $ar = array(&quot;status&quot; =&gt; &quot;test&quot;); // just for testing
      $json = json_encode($ar);
      return $json; // nothing - server error 500
  }
  
  </code></pre>
  <p>Any hints or tips? Any help is appreciated.</p>
  <p>Update:</p>
  <p>At one point, there was this error, which is now gone:</p>
  <pre><code>-][-][-][error][yii\web\HttpException:400] yii\web\BadRequestHttpException: Invalid JSON data in request body: Syntax error. in /home/examplecom/www/dev.sitename/vendor/yiisoft/yii2/web/JsonParser.php:60
  </code></pre>
  <p>I posted the data in body and in the header. The header payload was received successfully and displayed in web.log:</p>
  <pre><code>$_SERVER = [
   'HTTP_PAYLOAD' =&gt; ...
  ]
  </code></pre>
  <p>Now I've simplified my post data, and got this error:</p>
  <pre><code>[-][795][iaaukc5i9h7vq69afgfs13h0l0rv10se][error][Error] Error: Class 'zeixag\pdfcreator\controllers\Controller' not found in /home/examplecom/www/dev.sitename/zeixag/pdf-creator/src/controllers/PagelistController.php:26
  Stack trace:
  #0 /home/examplecom/www/dev.sitename/vendor/composer/ClassLoader.php(478): include()
  #1 /home/examplecom/www/dev.sitename/vendor/composer/ClassLoader.php(346): Composer\Autoload\includeFile('/home/examplecom/w...')
  #2 [internal function]: Composer\Autoload\ClassLoader-&gt;loadClass('zeixag\\pdfcreat...')
  #3 [internal function]: spl_autoload_call('zeixag\\pdfcreat...')
  #4 /home/examplecom/www/dev.sitename/vendor/yiisoft/yii2/base/Module.php(643): class_exists('zeixag\\pdfcreat...')
  #5 /home/examplecom/www/dev.sitename/vendor/yiisoft/yii2/base/Module.php(602): yii\base\Module-&gt;createControllerByID('pagelist')
  #6 /home/examplecom/www/dev.sitename/vendor/yiisoft/yii2/base/Module.php(594): yii\base\Module-&gt;createController('')
  #7 /home/examplecom/www/dev.sitename/vendor/yiisoft/yii2/base/Module.php(528): yii\base\Module-&gt;createController('pagelist')
  #8 /home/examplecom/www/dev.sitename/vendor/craftcms/cms/src/web/Application.php(277): yii\base\Module-&gt;runAction('pdf-creator/pag...', Array)
  #9 /home/examplecom/www/dev.sitename/vendor/craftcms/cms/src/web/Application.php(586): craft\web\Application-&gt;runAction('pdf-creator/pag...', Array)
  #10 /home/examplecom/www/dev.sitename/vendor/craftcms/cms/src/web/Application.php(256): craft\web\Application-&gt;_processActionRequest(Object(craft\web\Request))
  #11 /home/examplecom/www/dev.sitename/vendor/yiisoft/yii2/base/Application.php(392): craft\web\Application-&gt;handleRequest(Object(craft\web\Request))
  #12 /home/examplecom/www/dev.sitename/web/index.php(21): yii\base\Application-&gt;run()
  #13 {main}
  2021-09-28 09:16:38 [-][795][iaaukc5i9h7vq69afgfs13h0l0rv10se][info][application] $_GET = [
      'p' =&gt; 'cms/actions/pdf-creator/pagelist'
  ]
  </code></pre>
  <p>While the first error (malformed JSON) made 100% sense to me, this has me baffled.</p>
  <p>It seems like Craft assumes my controllers are not in the vendor folder, but in root. Why? I only placed it in root when I installed it via Composer. How can I fix this? I already removed and re-installed the plugin twice.</p>
  
Comments.ContentLicense: CC BY-SA 4.0
Comments.CreationDate: 2021-09-27T13:58:16.973Z
Comments.Text: can't you see anything on network response on browser? what about /storage/logs/web.log?
Comments.UserDisplayName: user9975
Comments.ContentLicense: CC BY-SA 4.0
Comments.CreationDate: 2021-09-28T12:13:34.290Z
Comments.Text: for custom plugin added to composer locally via path, you should composer remove package/name and composer require package/name on command line to reinstall that plugin.  have you tried this?
Comments.UserDisplayName: user9975
Comments.ContentLicense: CC BY-SA 4.0
Comments.CreationDate: 2021-09-29T08:29:10.603Z
Comments.Text: This doesn't answer your question, but given your use case, have you considered using a third-party plugin? I've not tried any of these myself but there's a couple here that sound like they'll do what you need: https://plugins.craftcms.com/search?q=pdf (https://plugins.craftcms.com/super-pdf and https://plugins.craftcms.com/document-helpers specifically)
Comments.UserDisplayName: James Smith
Tags: <plugin-development><controller>
Title: Posting data from CP to Plugin controller results in server error 500
ThreadText: # Posting data from CP to Plugin controller results in server error 500
  
  (Asked by: username_0 on 2021-09-27)
  
  I am currently trying to write my 1st Craft plugin.
  
  The goal (simplified):
  Backend users select a bunch of pages in the CP, and create a PDF. The result is an annual report that's optimized for print (so each year = 1 site / group, and each Craft page = 1 PDF "chapter").
  
  I've used plugin factory for scaffolding, and have the two main pieces together:
  
  PHP: get URL-list from outside, fetch pages via cURL, extract content via xPath, re-arrange HTML-headings (h1 becomes h2 etc.), create a table of contents and finally store the PDF somewhere (mPDF library was used).
  
  Craft CP: Create a sortable pages list with sortable.js, and a button that POSTs the JSON.stringify list with fetch to my plugin controller action.
  
  No matter what I do, the controller responds with server error 500.
  I've tried cors or no-cors, fiddled around with Apache headers (allow-origin et al), even used a simple old-school form instead of JS, but I was never able to get an HTTP result code 200 instead of 500.
  
  I have tried it locally first, then remote. What I'm trying to do is certainly not rocket science, I guess I either miss something that's not covered in the Craft docs, or have some server settings wrong (or maybe it's documented, but rather kinda "hidden" somewhere I didn't look yet).
  
  tl;dr: Posting data from my plugin CP to the plugin's controller action ends up with server error 500, and I'm stuck here. That's the last stumbling block to get my plugin finished, at least for a rough demo / MVP.
  
  JS inside my custom plugin templates/index.twig:
  
  ```
  button.addEventListener('click', event => {
    const newList = el.children;
    let ar = [];
    Array.from(newList).forEach((item) => {
      let url = item.querySelector('a').attributes.href.value;
      ar.push(url);
    })
  
    const pageSelection = JSON.stringify(ar);
    const target = '/actions/pdf-creator/default/get-page-selection';
  
    window.csrfTokenName = "{{ craft.app.config.general.csrfTokenName|e('js') }}";
    window.csrfTokenValue = "{{ craft.app.request.csrfToken|e('js') }}";
  
    let data = {};
    data[csrfTokenName] = csrfTokenValue;
    data["selection"] = pageSelection;
  
    fetch(target, {
      headers: {
        "Content-Type": "application/json",
        "Accept": "application/json, text-plain",
        "CRAFT_CSRF_TOKEN": window.csrfTokenValue,
      },
      method: 'POST',
      mode: 'no-cors',
      body: data
    })
      .then(response => getHeaders(response))
      .then(response => console.log(response))
  });
  
  ```
  
  My controller action: (inside myplugin/src/controllers/DefaultController.php)
  
  ```
  public function actionGetPageSelection() {
      $this->requirePostRequest();
      $post = Craft::$app->getRequest();
      //    $rawBody = Craft::$app->getRequest()->getRawBody(); // doesn't work either
      $ar = array("status" => "test"); // just for testing
      $json = json_encode($ar);
      return $json; // nothing - server error 500
  }
  
  ```
  
  Any hints or tips? Any help is appreciated.
  
  Update:
  
  At one point, there was this error, which is now gone:
  
  ```
  -][-][-][error][yii\web\HttpException:400] yii\web\BadRequestHttpException: Invalid JSON data in request body: Syntax error. in /home/examplecom/www/dev.sitename/vendor/yiisoft/yii2/web/JsonParser.php:60
  
  ```
  
  I posted the data in body and in the header. The header payload was received successfully and displayed in web.log:
  
  ```
  $_SERVER = [
   'HTTP_PAYLOAD' => ...
  ]
  
  ```
  
  Now I've simplified my post data, and got this error:
  
  ```
  [-][795][iaaukc5i9h7vq69afgfs13h0l0rv10se][error][Error] Error: Class 'zeixag\pdfcreator\controllers\Controller' not found in /home/examplecom/www/dev.sitename/zeixag/pdf-creator/src/controllers/PagelistController.php:26
  Stack trace:
  #0 /home/examplecom/www/dev.sitename/vendor/composer/ClassLoader.php(478): include()
  #1 /home/examplecom/www/dev.sitename/vendor/composer/ClassLoader.php(346): Composer\Autoload\includeFile('/home/examplecom/w...')
  #2 [internal function]: Composer\Autoload\ClassLoader->loadClass('zeixag\\pdfcreat...')
  #3 [internal function]: spl_autoload_call('zeixag\\pdfcreat...')
  #4 /home/examplecom/www/dev.sitename/vendor/yiisoft/yii2/base/Module.php(643): class_exists('zeixag\\pdfcreat...')
  #5 /home/examplecom/www/dev.sitename/vendor/yiisoft/yii2/base/Module.php(602): yii\base\Module->createControllerByID('pagelist')
  #6 /home/examplecom/www/dev.sitename/vendor/yiisoft/yii2/base/Module.php(594): yii\base\Module->createController('')
  #7 /home/examplecom/www/dev.sitename/vendor/yiisoft/yii2/base/Module.php(528): yii\base\Module->createController('pagelist')
  #8 /home/examplecom/www/dev.sitename/vendor/craftcms/cms/src/web/Application.php(277): yii\base\Module->runAction('pdf-creator/pag...', Array)
  #9 /home/examplecom/www/dev.sitename/vendor/craftcms/cms/src/web/Application.php(586): craft\web\Application->runAction('pdf-creator/pag...', Array)
  #10 /home/examplecom/www/dev.sitename/vendor/craftcms/cms/src/web/Application.php(256): craft\web\Application->_processActionRequest(Object(craft\web\Request))
  #11 /home/examplecom/www/dev.sitename/vendor/yiisoft/yii2/base/Application.php(392): craft\web\Application->handleRequest(Object(craft\web\Request))
  #12 /home/examplecom/www/dev.sitename/web/index.php(21): yii\base\Application->run()
  #13 {main}
  2021-09-28 09:16:38 [-][795][iaaukc5i9h7vq69afgfs13h0l0rv10se][info][application] $_GET = [
      'p' => 'cms/actions/pdf-creator/pagelist'
  ]
  
  ```
  
  While the first error (malformed JSON) made 100% sense to me, this has me baffled.
  
  It seems like Craft assumes my controllers are not in the vendor folder, but in root. Why? I only placed it in root when I installed it via Composer. How can I fix this? I already removed and re-installed the plugin twice.
  
  **Tags:** plugin-development, controller
  
  > **Comments:**
  > **username_1**: can't you see anything on network response on browser? what about /storage/logs/web.log?  
  > **username_1**: for custom plugin added to composer locally via path, you should composer remove package/name and composer require package/name on command line to reinstall that plugin.  have you tried this?  
  > **username_2**: This doesn't answer your question, but given your use case, have you considered using a third-party plugin? I've not tried any of these myself but there's a couple here that sound like they'll do what you need: https://plugins.craftcms.com/search?q=pdf (https://plugins.craftcms.com/super-pdf and https://plugins.craftcms.com/document-helpers specifically)  
  
  ---
  
  ## 1 answers
  
  ### Answer by username_3 on 2021-09-29. Score: 1
  
  There are two distinct problems here, the first with the formatting of your POST request body and the second with the location / routing of your contoller. You were getting the first error because your POST request contained invalid JSON, which means Yii2 returns an error before the request even reaches your controller. When you simplified your request for testing purposes, you probably removed that error, so now the problem is that Yii2 can't reach your controller.
  
  ## Fixing the POST request
  
  This is only a minor error – you have the `Content-Type: application/json` header in your request, but you're not actually passing JSON.
  
  ```
  let data = {};
  data[csrfTokenName] = csrfTokenValue;
  data["selection"] = pageSelection;
  
  fetch(target, {
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json, text-plain",
      "CRAFT_CSRF_TOKEN": window.csrfTokenValue,
    },
    method: 'POST',
    mode: 'no-cors',
    body: data
  })
  
  ```
  
  Note that `data` is not a JSON string, but an object with two properties. Only the `pageSelection` property contains a JSON string. `fetch` doesn't automatically stringify (encode) data, so when you give it an object, the default [Object.prototype.toString](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString) method is used to convert it for the request. In your case, this will return `[object Object]`, which is what the server receives. This isn't valid JSON, so you get an error.
  
  The solution is to stringify your object:
  
  ```javascript
  body: JSON.stringify(data),
  
  ```
  
  You don't want to encode the URLs to JSON twice, so make sure to remove this line and instead add the array with URls directly to the `data` object.
  
  ```javascript
  const pageSelection = JSON.stringify(ar);
  
  ```
  
  ---
  
  To debug errors like this, using the network tab in your browser console is essential. A quick test will show you the problem. In the console, try this code:
  
  ```javascript
  fetch('/test', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: { foo: 'bar'}
  })
  
  ```
  
  Now check the network tab, click on the fetch request and check the request tab for that request. It will show you that the body isn't a JSON string, but `[object Object]`.
  
  ## Fixing the controller routing
  
  The correct setup depends on what you're trying to build – a plugin that can be installed by others using Composer, or a module that is specific to a single site and included in the repository of the site?
  
  * For a plugin: See [How to build a plugin](https://craftcms.com/docs/3.x/extend/plugin-guide.html). Make sure to set up your plugin folder according to this guide, and include it with composer using a local path in a test site (as specified in the guide). In the plugin factory, make sure you selected *Craft CMS Plugin version 3.x*.
  * For a site-specific module: See [How to build a module](https://craftcms.com/docs/3.x/extend/module-guide.html). Your module files go in the `modules/` directory and you have to adjust the `autoload` information in the `composer.json` of your site so it knows to load modules from that folder. In the plugin factory, make sure you selected *Craft CMS Module version 3.x*.
  
  If you're having trouble, I recommend **not** to use the plugin factory. Instead, start from scratch and go through the guide step-by-step. The plugin factory is really a power-user tool and includes more than you need, in particular if you're just starting out.
  
  
  ---
  
  