onTheInternet
onTheInternet

Reputation: 7263

FLASK REST API returns 400 on POST

I'm building a REST API for a simple Todo application using flask and SQLAlchemy as my ORM. I am testing my API using Postman. I'm on a windows 10 64-bit machine.

A GET request works and returns the data that I've entered into my database using python.

Get Request

I'd like to try to add a task now. But when I POST my request, I receive an error.

POST request

My route in flask looks like this.

#add task
@app.route('/todo/api/v1.0/tasks', methods=['POST'])
def create_task():
    if not request.json or not 'title' in request.json:
        raise InvalidUsage('Not a valid task!', status_code=400)
    task = {
        'title': request.json['title'],
        'description': request.json['description'],
        'done': False
    }
    Todo.add_todo(task)
    return jsonify({'task': task}), 201

And the method it's calling on the Todo object looks like this.

def add_todo(_title, _description):
    new_todo = Todo(title=_title, description=_description , completed = 0)
    db.session.add(new_todo)
    db.session.commit()

What I've tried

I thought that maybe the ' in my Postman Params was causing an issue so I removed them. But I still get the same error.

Then I thought that maybe the way that Postman was sending the POST was incorrect so I checked to make sure that the Content-Type headers was correct. It is set to application/json

Finally, to confirm that the issue was that flask didn't like the request, I removed the check in the add task route to make sure the request had a title. So it looks like this.

if not request.json:

And I get the same error. So I think that the problem must be with how I'm actually sending the POST rather than some kind of formatting issue.

My entire code looks like this.

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import json
from flask import jsonify
from flask import request

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///todo.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

class Todo(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(300), unique=False, nullable=False)
    description = db.Column(db.String(), unique=False, nullable=False)
    completed = db.Column(db.Boolean, nullable=False)

    def json(self):
        return {'id': self.id,'title': self.title, 'description': self.description, 'completed': self.completed}

    def add_todo(_title, _description):
        new_todo = Todo(title=_title, description=_description , completed = 0)
        db.session.add(new_todo)
        db.session.commit()

    def get_all_tasks():
        return [Todo.json(todo) for todo in Todo.query.all()]

    def get_task(_id):
        task = Todo.query.filter_by(id=_id).first()
        if task is not None:
            return Todo.json(task)
        else:
            raise InvalidUsage('No task found', status_code=400)

    def __repr__(self):
        return f"Todo('{self.title}')"




class InvalidUsage(Exception):
    status_code = 400

    def __init__(self, message, status_code=None, payload=None):
        Exception.__init__(self)
        self.message = message
        if status_code is not None:
            self.status_code = status_code
        self.payload = payload

    def to_dict(self):
        rv = dict(self.payload or ())
        rv['message'] = self.message
        return rv

@app.route('/')
def hello_world():
    return 'Hello to the World of Flask!'

#get all tasks
@app.route('/todo/api/v1.0/tasks', methods=['GET'])
def get_tasks():
    return_value = Todo.get_all_tasks()
    return jsonify({'tasks': return_value})

#get specific task
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['GET'])
def get_task(task_id):
    task = Todo.get_task(task_id)
    #if len(task) == 0:
        #raise InvalidUsage('No such task', status_code=404)
    return jsonify({'task': task})

#add task
@app.route('/todo/api/v1.0/tasks', methods=['POST'])
def create_task():
    if not request.json or not 'title' in request.json:
        raise InvalidUsage('Not a valid task!', status_code=400)
    task = {
        'title': request.json['title'],
        'description': request.json['description'],
        'done': False
    }
    Todo.add_todo(task)
    return jsonify({'task': task}), 201

#update task
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['PUT'])
def update_task(task_id):
    task = [task for task in tasks if task['id'] == task_id]
    if len(task) == 0:
        raise InvalidUsage('No provided updated', status_code=400)
    if not request.json:
        raise InvalidUsage('request not valid json', status_code=400)
    if 'title' in request.json and type(request.json['title']) != unicode:
        raise InvalidUsage('title not unicode', status_code=400)
    if 'description' in request.json and type(request.json['description']) != unicode:
        raise InvalidUsage('description not unicode', status_code=400)
    if 'done' in request.json and type(request.json['done']) is not bool:
        raise InvalidUsage('done not boolean', status_code=400)
    task[0]['title'] = request.json.get('title', task[0]['title'])
    task[0]['description'] = request.json.get('description', task[0]['description'])
    task[0]['done'] = request.json.get('done', task[0]['done'])
    return jsonify({'task': task[0]})

#delete task
@app.route('/todo/api/v1.0/tasks/<int:task_id>', methods=['DELETE'])
def delete_task(task_id):
    task = [task for task in tasks if task['id'] == task_id]
    if len(task) == 0:
        raise InvalidUsage('No task to delete', status_code=400)
    tasks.remove(task[0])
    return jsonify({'result': True})


@app.errorhandler(InvalidUsage)
def handle_invalid_usage(error):
    response = jsonify(error.to_dict())
    response.status_code = error.status_code
    return response

if __name__ == '__main__':
    app.run(debug=True)

EDIT:

Turns out I wasn't setting the request type in POSTMAN correctly. I've updated it to 'application/json' in the header. Now I'm receiving a different error.

Bad Request Failed to decode JSON object: Expecting value: line 1 column 1 (char 0)

I've tried all the previous steps as before but I continue to get this error.

Badrequest

EDIT 2:

Per a response below, I tried putting the values into the body of the POST. But I still get back a 400 response.

body

Upvotes: 2

Views: 1530

Answers (1)

Dušan Maďar
Dušan Maďar

Reputation: 9909

From the image [second postman screenshot] it looks like you pass data in query string but create_task() expects them in request body.

Either replace all occurrences of request.json with request.args in create_task() (to make it work with query params) or leave it as it is and send data in request body.

curl -X POST http://localhost:5000/todo/api/v1.0/tasks \
-H "Content-Type: application/json" \
-d '{"title":"Learn more flask","description":"its supper fun"}'

Also, take a look at Get the data received in a Flask request.


EDITED

Update your add_todo to something like

@classmethod
def add_todo(cls, task):
    new_todo = cls(title=task["title"], description=task["description"], completed=0)
    db.session.add(new_todo)
    db.session.commit()

Related: generalised insert into sqlalchemy using dictionary.

Upvotes: 2

Related Questions