Sanic can't recognize post request

Hi. I’m using Vue.js and Sanic to make a web application. However, Sanic doesn’t recognize post requests from vue.js. It recognizes post request as options method. How can I solve this problem?

Can you post your relevant snippets of your endpoint and your vue client request?

`
@app.route("/respond", methods=[‘POST’, ‘OPTIONS’])
async def respond_class(request):
“”"
설명 : 챗봇에게 답변을 요청하는 메소드
:return:
“”"
# jpype.attachThreadToJVM()

def find_dialog_box_name_by_uid(uid):
    if len(uid) < 20:
        dialog_box = dialog_box_collection.find_one({"uid": uid})
    else:
        dialog_box = dialog_box_collection.find_one({"uid": uuid.UUID(uid)})

    if dialog_box is None:
        dialog_box = dict()
        dialog_box["boxName"] = "unknown"

    return dialog_box["boxName"]

try:
    logging.info("respond is called")
    print(request)
    print(request.url)
    print(request.json)
    request_json = request.json

    chatbotUID = uid_formatter(request_json["chatbotUID"])
    masterUID = uid_formatter(request_json["masterUID"])
    originUID = uid_formatter(request_json["originUID"])
    room = request_json["room"]
    locale = request_json["locale"]
    context = request_json["context"]

    input = request_json["input"]
    start = time.time()
    response = Response(input, masterUID, chatbotUID, context, locale, originUID, room)
    logging.info("initialization_time : " + str(time.time() - start))

    start = time.time()
    # 같은 문장이 있는지 처리하는 로직을 앞단에서 먼저 수행해 봄.
    if input == "start_conversation":
        logging.info("같은 문장이 있는지 처리하는 로직을 앞단에서 먼저 수행하지 않음.")

        thread1 = Greenlet.spawn(response.initial_data_setting)
        gevent.joinall([thread1])

        thread = Greenlet.spawn(response.respond)
        gevent.joinall([thread])
        response_list, resultContext, boxUID, score = thread.value

        result = {
            "result": True,
            "response_list": response_list,
            "boxUID": boxUID,
            "similarity": float(score),
            "boxName": find_dialog_box_name_by_uid(boxUID),
            "context": resultContext
        }

        if "goToInteractionActivated" in resultContext.keys() and resultContext["goToInteractionActivated"] is True:
            result["boxUID"] = resultContext["goToUIDTextString"]
            resultContext["goToInteractionActivated"] = False
            result["context"] = resultContext
    else:
        logging.info("같은 문장이 있는지 처리하는 로직을 앞단에서 수행")

        thread1 = Greenlet.spawn(response.initial_data_setting)
        gevent.joinall([thread1])

        thread = Greenlet.spawn(response.check_same_sentence_in_all_dialog_boxes)
        gevent.joinall([thread])
        response_list, resultContext, boxUID, score = thread.value

        if len(response_list) == 0 and resultContext is None:
            logging.info("같은 문장 찾기 로직 수행 실패. inference 작업 시작")

            thread = Greenlet.spawn(response.respond)
            gevent.joinall([thread])
            response_list, resultContext, boxUID, score = thread.value

            result = {
                "result": True,
                "response_list": response_list,
                "boxUID": boxUID,
                "similarity": float(score),
                "boxName": find_dialog_box_name_by_uid(boxUID),
                "context": resultContext
            }

            if "goToInteractionActivated" in resultContext.keys() and resultContext[
                "goToInteractionActivated"] is True:
                result["boxUID"] = resultContext["goToUIDTextString"]
                resultContext["goToInteractionActivated"] = False
                result["context"] = resultContext
        else:
            result = {
                "result": True,
                "response_list": response_list,
                "boxUID": boxUID,
                "similarity": float(score),
                "boxName": find_dialog_box_name_by_uid(boxUID),
                "context": resultContext
            }

            if "goToInteractionActivated" in resultContext.keys() and resultContext[
                "goToInteractionActivated"] is True:
                result["boxUID"] = resultContext["goToUIDTextString"]
                resultContext["goToInteractionActivated"] = False
                result["context"] = resultContext

    logging.info("processing_time : " + str(time.time() - start))
    return response.json(result)
except Exception as e:
    logging.error(e)
    context = request_json["context"]
    result = {
        "result": False,
        "response_list" : [],
        "boxUID": "Error!",
        "similarity": float(0),
        "boxName": "Error!",
        "context" : context
    }
    return response.json(result)

`

this is my endpoint.

`
this.$http.post(response_url, this.computedPayload)

.then(res => {

  // this.$log.info('대화 시작 메시지: ', res.data)

  if (res.data.result) {

    // this.$log.success("리스폰스 성공")

    this.updateContext(res.data.context)

    this.delayedPushList(res.data.response_list)

    this.scrollToBottom() // 채팅 스크롤 내리기

  } else {

    this.$log.error('리스폰스 실패')

  }

})

.catch(err => {

  console.log(err)

})

`

and this is my vue client request

although I’ve changed route method as options and post, I can’t receive json data from request. I don’t know how to get json request from options method.

I think there’s a small problem in your code (I don’t know if it’s related to the issue, but might be).

To make a proper response, you need to import the response module from Sanic and use it as you did in the end of your endpoint:

    # ...
    return response.json(result)

The thing is, in the middle of your endpoint code, you reassign the response variable (in this case, a module) to something else:

   # ...
    input = request_json["input"]
    start = time.time()
    response = Response(input, masterUID, chatbotUID, context, locale, originUID, room)  # <--- HERE
    logging.info("initialization_time : " + str(time.time() - start))
   # ...

If this Response object is not derived from the Sanic response module (which I don’t believe it is), you might have some problems … Also, I don’t know how asyncio and Greenlet get along, so I might try to simplify your endpoint code just testing reasons only (in case this continues to happen after you fix the response variable issue).

I’m also not able to see data send from Vue.js with axios on my server. Although my front-end does get the JSON response (albeit twice).

My Sanic server (running in a Docker container with FROM sanicframework/sanic:LTS and port 8080 mapped to 8081):

import os
from sanic import Sanic, response
from sanic_cors import CORS

app = Sanic()
CORS(app)  # , automatic_options=True

@app.route('/', methods=['GET', 'POST', 'OPTIONS'])
async def test(request):
    print("Received request:", request)
    print("args", request.args)

    return response.json({"foo": "test"})

if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=int(os.environ.get('PORT', 8080)))

My front-end sends this request:

axios
  .post('http://' + '172.16.0.88' + ':8081/', { data: { spam: "eey sexy" }})
  .then(response => (
          this.apiKO = response.data
  ))
  .catch(error => console.log(error))
  .finally(() => this.fetching = false)
},

And this is the output of my Docker container

[2020-02-15 21:53:33 +0000] [1] [INFO] Goin' Fast @ http://0.0.0.0:8080
[2020-02-15 21:53:33 +0000] [8] [INFO] Starting worker [8]
[2020-02-15 21:53:40 +0000] [8] [DEBUG] CORS: No CORS rule matches

Received request: <Request: OPTIONS />
args {}

[2020-02-15 21:53:40 +0000] [8] [DEBUG] CORS: Request to '/' matches CORS resource '/*'. Using options: {'origins': ['.*'], 'methods': 'DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT', 'allow_headers': ['.*'], 'expose_headers': None, 'supports_credentials': False, 'max_age': None, 'send_wildcard': False, 'automatic_options': False, 'vary_header': True, 'resources': '/*', 'intercept_exceptions': True, 'always_send': True}
[2020-02-15 21:53:40 +0000] - (sanic.access)[INFO][172.16.0.88:50246]: OPTIONS http://172.16.0.88:8081/  200 14


Received request: <Request: POST />
args {}

[2020-02-15 21:53:40 +0000] [8] [DEBUG] CORS: Request to '/' matches CORS resource '/*'. Using options: {'origins': ['.*'], 'methods': 'DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT', 'allow_headers': ['.*'], 'expose_headers': None, 'supports_credentials': False, 'max_age': None, 'send_wildcard': False, 'automatic_options': False, 'vary_header': True, 'resources': '/*', 'intercept_exceptions': True, 'always_send': True}
[2020-02-15 21:53:40 +0000] - (sanic.access)[INFO][172.16.0.88:50246]: POST http://172.16.0.88:8081/  200 14
[2020-02-15 21:53:45 +0000] [8] [DEBUG] KeepAlive Timeout. Closing connection.

My front-end gets the reply, so no problem there.

Two strange things to notice here:

  • Even though the front-end send data to Sanic, the server does not see it (args {}).
  • Sanic receives an OPTIONS and a POST request.

Extra: Just before trying Sanic, I was trying Flask + Gunicorn as server with the same front-end. However, Flask would only get 1 request (but it didn’t see the data):

<Request 'http://0.0.0.0:8081/' [POST]>
Got a request with argument: ImmutableMultiDict([])
{}

When I changed the axios data argument from { data: { spam: "eey sexy" }} to 'param1=value1&param2=value2', with Flask I saw:

<Request 'http://172.16.0.88:8081/' [POST]>
Got a request with argument: ImmutableMultiDict([])
{'param1': 'value1', 'param2': 'value2'}

but for Sanic it is still empty {}.

Let’s take this step by step to see where the problem is. I’ll build a simplified end point, then add axios, and then see where your implementation differs.

Sample Backend

from sanic import Sanic, response

app = Sanic(__file__)


@app.route("/", methods=["GET", "POST", "OPTIONS"])
async def test(request):
    print("Request headers:", request.headers)
    print("Request method:", request.method)
    print("Request args:", request.args)
    if request.method == "OPTIONS":
        return response.text("", status=204)
    print("Request body:", request.body)
    print("Request json:", request.json)
    return response.json(request.json)


app.run(debug=True)

TESTING

We can send all sorts of requests. First, let’s start simple.

### CLIENT REQUEST
$ curl localhost:8000
null

### SERVER LOGS
Request headers: <Header('host': 'localhost:8000', 'user-agent': 'curl/7.67.0', 'accept': '*/*')>
Request method: GET
Request args: {}
Request body: b''
Request json: None

Now, we will try with request arguments.

### CLIENT REQUEST
$ curl localhost:8000\?foo=bar
null

### SERVER LOGS
Request headers: <Header('host': 'localhost:8000', 'user-agent': 'curl/7.67.0', 'accept': '*/*')>
Request method: GET
Request args: {'foo': ['bar']}
Request body: b''
Request json: None

This works for POST as well.

### CLIENT REQUEST
$ curl localhost:8000\?foo=bar -X POST
null

### SERVER LOGS
Request headers: <Header('host': 'localhost:8000', 'user-agent': 'curl/7.67.0', 'accept': '*/*')>
Request method: POST
Request args: {'foo': ['bar']}
Request body: b''
Request json: None

And, I can also send OPTIONS:

### CLIENT REQUEST
$ curl localhost:8000 -X OPTIONS -i
HTTP/1.1 204 No Content
Connection: keep-alive
Keep-Alive: 5
Content-Type: text/plain; charset=utf-8


### SERVER LOGS
Request headers: <Header('host': 'localhost:8000', 'user-agent': 'curl/7.67.0', 'accept': '*/*')>
Request method: OPTIONS
Request args: {}

Let’s try sending some JSON data

### CLIENT REQUEST
$ curl localhost:8000\?foo=bar -X POST -d '{"hello": "world"}' -H 'Content-Type: application/json' -i
HTTP/1.1 200 OK
Connection: keep-alive
Keep-Alive: 5
Content-Length: 17
Content-Type: application/json

{"hello":"world"}

### SERVER LOGS
Request headers: <Header('host': 'localhost:8000', 'user-agent': 'curl/7.67.0', 'accept': '*/*', 'content-type': 'application/json', 'content-length': '18')>
Request method: POST
Request args: {'foo': ['bar']}
Request body: b'{"hello": "world"}'
Request json: {'hello': 'world'}

What if we forget the Content-Type header and send form data?

### CLIENT REQUEST
$ curl localhost:8000\?foo=bar -X POST -d '{"hello": "world"}' -i                                    
HTTP/1.1 200 OK
Connection: keep-alive
Keep-Alive: 5
Content-Length: 17
Content-Type: application/json

{"hello":"world"}


### SERVER LOGS
Request headers: <Header('host': 'localhost:8000', 'user-agent': 'curl/7.67.0', 'accept': '*/*', 'content-length': '18', 'content-type': 'application/x-www-form-urlencoded')>
Request method: POST
Request args: {'foo': ['bar']}
Request body: b'{"hello": "world"}'
Request json: {'hello': 'world'}

CONCLUSION

Everything seems to be working as expected. We can send sanic keyword arguments and JSON data and it responds appropriately.

SAMPLE AXIOS CLIENT

Now let’s try the same with axios instead of curl. Here is the script I am going to use as my client:

const axios = require('axios');

const url = process.argv[2];
const method = process.argv[3] || 'get';
const data = process.argv[4] ? {hello: 'world'} : null;

axios(url, {
    method,
    data,
}).then(response => {
    const {status, headers, data} = response;
    console.log({
        status,
        headers,
        data,
    });
});

TESTING

Let’s see it working, same as before.

### CLIENT REQUEST
$ node client.js http://localhost:8000                     
{ status: 200,
  headers:
   { connection: 'close',
     'content-length': '4',
     'content-type': 'application/json' },
  data: null }


### SERVER LOGS
Request headers: <Header('accept': 'application/json, text/plain, */*', 'user-agent': 'axios/0.19.2', 'host': 'localhost:8000', 'connection': 'close')>
Request method: GET
Request args: {}
Request body: b''
Request json: None

Now, we will try with request arguments.

### CLIENT REQUEST
$ node client.js http://localhost:8000\?foo\=bar
{ status: 200,
  headers:
   { connection: 'close',
     'content-length': '4',
     'content-type': 'application/json' },
  data: null }


### SERVER LOGS
Request headers: <Header('accept': 'application/json, text/plain, */*', 'user-agent': 'axios/0.19.2', 'host': 'localhost:8000', 'connection': 'close')>
Request method: GET
Request args: {'foo': ['bar']}
Request body: b''
Request json: None

This works for POST as well.

### CLIENT REQUEST
$ node client.js http://localhost:8000\?foo\=bar post
{ status: 200,
  headers:
   { connection: 'close',
     'content-length': '4',
     'content-type': 'application/json' },
  data: null }

### SERVER LOGS
Request headers: <Header('accept': 'application/json, text/plain, */*', 'content-type': 'application/x-www-form-urlencoded', 'user-agent': 'axios/0.19.2', 'host': 'localhost:8000', 'connection': 'close', 'content-length': '0')>
Request method: POST
Request args: {'foo': ['bar']}
Request body: b''
Request json: None

And, I can also send OPTIONS:

### CLIENT REQUEST
$ node client.js http://localhost:8000\?foo\=bar options
{ status: 204,
  headers:
   { connection: 'close',
     'content-type': 'text/plain; charset=utf-8' },
  data: '' }

### SERVER LOGS
Request headers: <Header('accept': 'application/json, text/plain, */*', 'user-agent': 'axios/0.19.2', 'host': 'localhost:8000', 'connection': 'close')>
Request method: OPTIONS
Request args: {'foo': ['bar']}

Let’s try sending some JSON data

### CLIENT REQUEST
$ node client.js http://localhost:8000\?foo\=bar post true
{ status: 200,
  headers:
   { connection: 'close',
     'content-length': '17',
     'content-type': 'application/json' },
  data: { hello: 'world' } }


### SERVER LOGS
Request headers: <Header('accept': 'application/json, text/plain, */*', 'content-type': 'application/json;charset=utf-8', 'user-agent': 'axios/0.19.2', 'content-length': '17', 'host': 'localhost:8000', 'connection': 'close')>
Request method: POST
Request args: {'foo': ['bar']}
Request body: b'{"hello":"world"}'
Request json: {'hello': 'world'}

CONCLUSION

It seems that our axios client is able to handle sending requests to sanic just as the raw curl requests.

What is happening in your case?

First, the sanicframework/sanic:LTS is in need of updating. That is on me. It is still running 18.12LTS and not 19.12LTS. But that should not matter for such a small test. There was some changes to how request arguments are handled, but Sanic 18.12 still has the ability to parse args just fine. The API is just slightly different.

Second, I would suggest catching OPTIONS and responding with an empty response as in my example.

When looking at your example, I do not see any args data being passed. You can pass request.args using ?foo=bar or request.json by passing data as in your example.

Please let me know if I am missing something in your question.

Thank you very much for the elaborate response. You didn’t only solve my problem, but also gave me a better understanding of what’s going on behind the scenes.

I’m ashamed to say it, but I was not aware that JSON data is accessed with request.json, which solved my issue (when using the 18.12 LTS Docker container and with non-Docker 19.6.3, due to lacking CORS support).
In the docs, I saw that JSON was used a lot for sending back replies, but somehow I assumed all .json I saw was related to responding, and not parsing data…

About receiving an OPTIONS and POST request when using Sanic and only a POST request when using Flask + Gunicorn, I just checked the network tab in my browser and I see that sending a request through axios to a Flask server does send an OPTIONS request. It seems that Flask automatically replies with a 200 reply of type html.
As I’m not that knowledgeable in this area, is there a reason why Flask and Sanic take a different approach to this (Flask taking care of it behind the scenes)? And why would you prefer sending a 204 back instead of 200?

In general it is better to be explicit as opposed to implicit when handling requests. While I can’t speak to the original design decisions of either, my expectation for sanic is that a route, that is a method combined with a path, if it is not defined will reply with a 404.

It is my opinion that this is the proper way to do things and reduces risk around implementing routes that are not supposed to exist. This is very important with RESTful APIs where the handing of POST, GET, PUT, and DELETE have expected behaviors, but is perhaps less important than with GraphQL APIs.

Check this out for more information on how requests should behave in HTTP/1.1 requests. You’ll want to note that:

A server generating a successful response to OPTIONS SHOULD send any
header fields that might indicate optional features implemented by
the server and applicable to the target resource (e.g., Allow),
including potential extensions not defined by this specification.

So technically responding “200” to an OPTIONS request by the client is not necessarily wrong if the client sends a wildcard options request (OPTIONS *) which by the RFC should have an empty response and a content-length of 0, the more correct way to handle it from an API point of view would be to return a 405 if you have not specifically implemented an OPTIONS handler for your API (as it should be disallowed) or a 501, depending on whether you think the client or the server is responsible for the request.

Thanks for the explanation and link to HTTP/1.1 document. It has been helpful :slight_smile: