1

I'm learning full stack in Flask and am having trouble with a particular route from an API. The API being developed is a list of books and in particular I am trying to reach the data for a particular book, say book with ID = 8. The URI is http://127.0.0.1:5000/books/8. However this returns a 400 error (bad request).

I really can't spot what is going wrong. I have defined the route '/books/int:book_id' with methods GET and PATCH, so I would expect the route to work. I also see errors when I test the route with curl, for example:

curl -X PATCH -H "Content-Type: application/json" -d '{"rating":"1"}' http://127.0.0.1:5000/books/8

See below for the particular route in question:

@app.route('/books/<int:book_id>', methods=['GET', 'PATCH'])
def update_book_rating(book_id):
        
    body = request.get_json() 

    try:
        book = Book.query.filter_by(Book.id==book_id).one_or_none()
        if book is None:
           abort(404)
            
        if 'rating' in body:
            book.rating = int(body.get('rating'))

        book.update() #Class book in models.py has an update method which executes a commit()

        return jsonify({
            'success': True,
            'id': book.id
        })
        
    except Exception as e:
        print(e)
        abort(400)

If it helps, I am also adding the full code. Note that the Book object is defined in a separate file, which I won't put here.

import os
from flask import Flask, request, abort, jsonify
from flask_sqlalchemy import SQLAlchemy  # , or_
from flask_cors import CORS
import random

from models import setup_db, Book

BOOKS_PER_SHELF = 8

# @TODO: General Instructions
#   - As you're creating endpoints, define them and then search for 'TODO' within the frontend to update the endpoints there.
#     If you do not update the endpoints, the lab will not work - of no fault of your API code!
#   - Make sure for each route that you're thinking through when to abort and with which kind of error
#   - If you change any of the response body keys, make sure you update the frontend to correspond.

def paginate_books(request, selection):
        page = request.args.get('page', 1, type=int)
        start = (page - 1) * BOOKS_PER_SHELF
        end = start + BOOKS_PER_SHELF

        books = [book.format() for book in selection]
        current_books = books[start:end]

        return current_books


def create_app(test_config=None):
    # create and configure the app
    app = Flask(__name__)
    setup_db(app)
    CORS(app)

    # CORS Headers
    @app.after_request
    def after_request(response):
        response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization,true")
        response.headers.add("Access-Control-Allow-Methods", "GET,PUT,PATCH,POST,DELETE,OPTIONS")
        return response

    # @TODO: Write a route that retrivies all books, paginated.
    #         You can use the constant above to paginate by eight books.
    #         If you decide to change the number of books per page,
    #         update the frontend to handle additional books in the styling and pagination
    #         Response body keys: 'success', 'books' and 'total_books'
    # TEST: When completed, the webpage will display books including title, author, and rating shown as stars

    @app.route('/books', methods=['GET'])
    def get_books():
        
        selection = Book.query.order_by(Book.id).all()
        current_books = paginate_books(request, selection)

        if len(current_books) == 0:
            abort(404)

        return jsonify({
            'success': True,
            'books': current_books,
            'total_books': len(Book.query.all())
        })

    # @TODO: Write a route that will update a single book's rating.
    #         It should only be able to update the rating, not the entire representation
    #         and should follow API design principles regarding method and route.
    #         Response body keys: 'success'
    # TEST: When completed, you will be able to click on stars to update a book's rating and it will persist after refresh

    @app.route('/books/<int:book_id>', methods=['GET', 'PATCH'])
    def update_book_rating(book_id):
        
        body = request.get_json() 

        try:
            book = Book.query.filter_by(Book.id==book_id).one_or_none()
            if book is None:
                abort(404)
            
            if 'rating' in body:
                book.rating = int(body.get('rating')) 

            book.update() #Class book in models.py has an update method which executes a commit()

            return jsonify({
                'success': True,
                'id': book.id
            })
        
        except Exception as e:
            print(e)
            abort(400)


    # @TODO: Write a route that will delete a single book.
    #        Response body keys: 'success', 'deleted'(id of deleted book), 'books' and 'total_books'
    #        Response body keys: 'success', 'books' and 'total_books'

    @app.route('/delete/<int:book_id>', methods=['DELETE'])
    def delete_book(book_id):

        try:
            book = Book.query.filter_by(Book.id==book_id).one_or_none()

            if book is None:
                abort(404)

            book.delete()
            selection = Book.query.order_by(Book.id).all()
            current_books = paginate_books(request, selection)

            return jsonify({
                'success': True,
                'deleted': book_id,
                'books': current_books,
                'total_books': len(Book.query.all())
            })

        except:
            abort(422)


    # TEST: When completed, you will be able to delete a single book by clicking on the trashcan.

    # @TODO: Write a route that create a new book.
    #        Response body keys: 'success', 'created'(id of created book), 'books' and 'total_books'
    # TEST: When completed, you will be able to a new book using the form. Try doing so from the last page of books.
    #       Your new book should show up immediately after you submit it at the end of the page.

    @app.route('/books', methods=['POST'])
    def create_book():
        body = request.get_json()

        new_title = body.get('title', None)
        new_author = body.get('author', None)
        new_rating = body.get('rating', None)

        try:
            book = Book(title=new_title, author=new_author, rating=new_rating)
            book.insert()

            selection = Book.query.order_by(Book.id).all()
            current_books = paginate_books(request, selection)

            return jsonify({
                'success': True,
                'created': book.id,
                'books': current_books,
                'total_books': len(Book.query.all())
            })

        except:
            abort(422)

    @app.errorhandler(400)
    def bad_request(error):
        return jsonify({
            'success': False,
            'error': 400,
            'message': 'Server cannot or will not process the request due to client error (for example, malformed request syntax, invalid request message framing, or deceptive request routing).'
        }), 400
    
    @app.errorhandler(404)
    def not_found(error):
        return jsonify({
            'success': False,
            'error': 404,
            'message': 'resource not found'
        }), 404

    @app.errorhandler(405)
    def not_found(error):
        return jsonify({
            'success': False,
            'error': 405,
            'message': 'method not allowed'
        }), 405

    @app.errorhandler(422)
    def unprocessable(error):
        return jsonify({
            'success': False,
            'error': 422,
            'message': 'unprocessable'
        }), 422

    return app
Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
OdaTruth
  • 75
  • 12
  • Does an exception appear in the server log? – John Gordon Apr 02 '22 at 23:25
  • Yes: 127.0.0.1 - - [03/Apr/2022 00:04:37] "PATCH /books/8 HTTP/1.1" 400 - Thats what shows up. – OdaTruth Apr 03 '22 at 01:01
  • No, I meant the output from `print(e)` in the try/except block. – John Gordon Apr 03 '22 at 01:13
  • There's probably something wrong with the data being sent from the browser (see [this answer](https://stackoverflow.com/a/54169274/5320906)). Use the network tab in your browser's dev tools to inspect the data in the request. – snakecharmerb Apr 03 '22 at 06:29
  • @JohnGordon I just ran the script in VS Code and checked the terminal within the code editor. This is what I see: "location".../flaskr/__init__.py WARNING: this script is deprecated, please see git-completion.zsh "location".../flaskr/__init__.py Traceback (most recent call last): File "path.../flaskr/__init__.py", line 7, in from models import setup_db, Book ModuleNotFoundError: No module named 'models' – OdaTruth Apr 03 '22 at 14:05
  • @JohnGordon It seems there is an error referring to the models.py file I have which contains the class Book. However, VS Code is not saying there is anything wrong with the reference to the "models" file. The folder structure is: `backend - flaskr - __pycache__ - __init.py__ - models.py` The file with the code in my question is __init.py__ and the file which houses the Book class is models.py. – OdaTruth Apr 03 '22 at 14:05
  • @JohnGordon sorry the comments lose their formatting. Basically models.py sits at the same level as flaskr, and both flask and models.py sit under backend. The files __pycache__ and __init.py__ sit inside the flaskr folder. – OdaTruth Apr 03 '22 at 14:10
  • An update on this: I spotted the following error in terminal when trying to access the route http://127.0.0.1:5000/books/8. **filter_by() takes 1 positional argument but 2 were given** 127.0.0.1 - - [04/Apr/2022 17:51:52] "GET /books/8 HTTP/1.1" 400 - There seems to be something wrong with how I implemented the filter_by method, which is weird since I have done it that way before with no issues... – OdaTruth Apr 04 '22 at 17:04

1 Answers1

0

Were you sure to add CSRF token in your form? If not that can throw this error. You can see if this is the error by diving into the request details in developer tools Network --> Preview

If the issue is CSRF, see here for a good guide to adding CSRF protection with flask https://testdriven.io/blog/csrf-flask/

Bjc
  • 93
  • 6