0

I wrote a simple Seneca plugin that I will eventually use to listen for http messages:

buyer.js

module.exports = function buyer (){

    this.add('role:buyer, action: acceptOffer', function(msg, respond){
        respond(JSON.stringify({answer: msg.id}))
    })
}

When I run this with the following script:

index.js

require('seneca')()
    .use(require('./designs/offer/roles/buyer.js'))
    .listen()

I can now send POST requests to localhost:10101/act and use the plugin:

curl -d '{"role": "buyer", "action": "acceptOffer", "id": "12"}' http://localhost:10101/act
{"answer":"12"}

Here's where things get messy. I now want to make this http request through a web app, so I use axios to make a request like this from a web page:

App.vue

<template>
    <div id="app">
        <button @click="sendQuery"></button>
    </div>
</template>

<script>
 import axios from 'axios';

 export default {
     name: 'App',
     data(){
         return {
             postBody: {
                 id: '12',
                 role: 'buyer',
                 action: 'acceptOffer'
             }
         }
     },
     methods: {
         sendQuery: function(){
             var body = JSON.stringify(this.postBody)
             axios.post(`http://localhost:10101/act`, {
                 body: body
             })
         }
     }
 }
</script>

When I click the button to send the request, I get this error message from my browser console (after I enable CORS):

xhr.js?ec6c:178 POST http://localhost:10101/act 500 (Internal Server Error)
dispatchXhrRequest @ xhr.js?ec6c:178
xhrAdapter @ xhr.js?ec6c:12
dispatchRequest @ dispatchRequest.js?c4bb:59
Promise.then (async)
request @ Axios.js?5e65:51
Axios.(anonymous function) @ Axios.js?5e65:71
wrap @ bind.js?24ff:9
sendQuery @ App.vue?26cd:26
boundFn @ vue.esm.js?efeb:190
invoker @ vue.esm.js?efeb:2004
fn._withTask.fn._withTask @ vue.esm.js?efeb:1802
createError.js?16d0:16 Uncaught (in promise) Error: Request failed with status code 500
    at createError (createError.js?16d0:16)
    at settle (settle.js?db52:18)
    at XMLHttpRequest.handleLoad (xhr.js?ec6c:77)

Can anyone tell me why this works differently than curl? Why can't I get my response?

David J.
  • 1,753
  • 13
  • 47
  • 96
  • 1
    In `curl` you're sending an object that directly contains `role`, `action` and `id`. In axios you're sending an object that has a property called `body` which has the `json` string of that data. What happens if you just pass `this.postBody` directly to axios? – Luis Orduz Feb 13 '18 at 14:17

4 Answers4

3

Yup, tested locally and your problem indeed seems to be with Stringify, as mentioned in my comment, just send the data directly:

axios.post('http://localhost:10101/act', this.postBody)
Luis Orduz
  • 2,887
  • 1
  • 13
  • 19
  • Could you test this locally without altering CORS settings? What did you use to get past that, if not? – David J. Feb 15 '18 at 08:40
  • No, I needed to disable CORS checking in the browser. At least in Chrome where I tested, I've always had to do that for testing locally when the backend and the frontend are served by separate processes. – Luis Orduz Feb 15 '18 at 12:32
1

The curl does not work otherwise, you just do not pass the desired header in the query. The bottom line is that the server should understands in which format it should receive data. I see that in your code you give JSON. Therefore, specify the format of the transmitted data via the header.

For example, you should such request:

curl -H "Content-Type: application/json" localhost:10101/act

Your server (backend) must respond with exactly the same header.

Problem a CORS - problem a server. If you have problem with CORS, in your case, I think that your frontend works on a different port in contrast to api. In any case, you do not need to transfer the header for the CORS on the front-end side (although someone is trying to do this and usually it's a waste of time). You just need to monitor the type of data transmitted.

See example Axios get/post (never mind):

const configAxios = {
  headers: {
    'Content-Type': 'application/json',
  },
};
axios.post('api/categories', configAxios)
  .then((res) => {
    this.categories = res.data;
    console.log(res);
  })
  .catch((err) => {
    console.warn('error during http call', err);
  });

In your code you use JSON.stringify, don't do it, because Axios already use this features.

Server side

For example server-side. I like Symfony4 and it is used NelmioCorsBundle, look at the allow_origin: ['*']. It's pretty simple, if you use Symfony.

nelmio_cors:
    defaults:
        allow_credentials: false
        allow_origin: ['*']
        allow_headers: ['Content-Type']
        allow_methods: []
        expose_headers: []
        max_age: 0
        hosts: []
        origin_regex: false
        forced_allow_origin_value: ~
    paths:
        '^/api/':
            allow_origin: ['*']
            allow_headers: ['X-Custom-Auth', 'Content-Type', 'Authorization']
            allow_methods: ['POST', 'PUT', 'GET', 'DELETE']
            max_age: 3600
        '^/':
            origin_regex: true
            allow_origin: ['^http://localhost:[0-9]+']
            allow_headers: ['X-Custom-Auth', 'Content-Type']
            allow_methods: ['POST', 'PUT', 'GET', 'DELETE']
            max_age: 3600
            hosts: ['^api\.']

If you are not directly working with the server, then check with your supplier for this nuance.

This header can also be transmitted for example through Nginx, which is not the best idea.

For example, look at the:

add_header Access-Control-Allow-Origin *;

server {
    listen 8080;
    server_name site.local;
    root /var/www/site/public;

    location / {
       add_header Access-Control-Allow-Origin *;

        # try to serve file directly, fallback to index.php
        try_files $uri /index.php$is_args$args; 
    }

    location ~ ^/index\.php(/|$) {
        fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
        # When you are using symlinks to link the document root to the
        # current version of your application, you should pass the real
        # application path instead of the path to the symlink to PHP
        # FPM.
        # Otherwise, PHP's OPcache may not properly detect changes to
        # your PHP files (see https://github.com/zendtech/ZendOptimizerPlus/issues/126
        # for more information).
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        # Prevents URIs that include the front controller. This will 404:
        # http://domain.tld/index.php/some-path
        # Remove the internal directive to allow URIs like this
        internal;
    }

    # return 404 for all other php files not matching the front controller
    # this prevents access to other php files you don't want to be accessible.
    location ~ \.php$ {
        return 404;
    }

    error_log /var/log/nginx/project_error.log;
    access_log /var/log/nginx/project_access.log;
}

It is worth paying attention if there is no data passed it removes the Content-Type. The data must always be transmitted or be null. This is strange and it is misleading.

Dmitry S.
  • 3,766
  • 3
  • 18
  • 26
1

The following code has many issues:

var body = JSON.stringify(this.postBody)
axios.post(`http://localhost:10101/act`, {
  body: body
})

First, axios.post() returns a Promise. If your method is meant to be asynchronous, you should either await axios.post(…) and mark your method async or return axios.post(…).

Second, if you see the axios docs for axios.post(), the second parameter is the postData itself. What you have instructed AXIOS to do is send this JSON as the body:

{"body":"{\\"id\\":\\"12\\",\\"role\\":\\"buyer\\",\\"action\\":\\"acceptOffer\\"}"}

i.e., you are 1. wrapping the object you intend to send in yet another object 2. double-stringifying the data. Consider the result of parsing it once (which seneca will do for you):

> JSON.parse(s)
{ body: '{"id":"12","role":"buyer","action":"acceptOffer"}' }

The above data with the body key being a string is what Seneca is being sent and what you will have to create a handler for. Within your handler, you will have to unescape body again:

> JSON.parse(JSON.parse(s).body)
{ id: '12', role: 'buyer', action: 'acceptOffer' }

However, I do not know why this would cause an error with Seneca.js itself. It’s likely that seneca is throwing the error because you have not set any handlers for the the pattern where there is a body property with a string value. Maybe if you do something like this (I don’t know how to accept a string value as a pattern in seneca, this is probably wrong):

this.add('body:string', function(msg, respond){respond({value: msg.body})});

Guesses

Maybe you intended to write your axios call passing the unescaped data so that it gets encoded to JSON by axios properly:

var body = this.postBody; // Note *NOT USING JSON.stringify()*
axios.post('http://localhost:10101/act', body); // Note *NOT PASSING body KEY*

and in your handler you shouldn’t double encode the result either (probably, unsure about how seneca works):

module.exports = function buyer (){
    this.add('role:buyer, action: acceptOffer', function(msg, respond){
        respond({answer: msg.id}) // Note *NOT USING JSON.stringify*
    })
}
binki
  • 7,754
  • 5
  • 64
  • 110
  • Thanks for the helpful pointers. One more thing - I needed to turn off cors protection with a browser extension to test this. Is there some easier/safer way to do this? – David J. Feb 15 '18 at 08:03
  • 1
    How do you solve CORS when deploying non-locally? You can probably just serve the HTML through a local webserver which proxies `/act` through to Seneca which would cause the API and page to share their HTTP origin, bypassing the need for CORS. – binki Feb 15 '18 at 14:11
0

your axios request header must contain

header {
    'Content-Type' : 'application/x-www-form-urlencoded; charset=UTF-8;application/json'
}

This works for me.

upe
  • 1,862
  • 1
  • 19
  • 33
ganicvp7
  • 47
  • 4