2

I'm having a problem making requests to my endpoint /send-email from React. I have flask-mail configured to make the async call.

This is how frontend makes the request:

 emailClient(event, index){
  let {clientCount} = this.state;
  var headers = {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': true,
      Authorization: `Bearer ${window.localStorage.authToken}`
    }
  const {userId} = this.props
  const client = this.state.clients[index];
  const data = {
    client: client
  };
  const url = `${process.env.REACT_APP_WEB_SERVICE_URL}/send-email/${userId}`;
  axios.post(url, data, {headers: headers})
    .then((res) => {
      this.setState({
        clientCount: clientCount +1
      });
      console.log(data);
    })
    .catch((err) => {
    });
  };

rendering:

  render() {
    const orders = this.state.clients;
    const { clients, youtube_urls, loadedVideosCount, currentPlayingIndex } = this.state;

    return (
      <div>
        <h1 className="title is-1">Jukebox</h1>
          {this.state.isLoading}
          {
            clients.map((client, index) => {
               /* 
              Obtain preview from state.previews for current artist index 
              */
              const audio = youtube_urls[index]
              /* 
              Render current client data, and corresponding audio url
              */
              return(
                <div key={index}>
                <ul>
                  <li><font color="#C86428">Client: </font><strong><font color="#6f4e37"> { client.name } </font></strong></li>
                  <li><font color="#C86428">Phone: </font><strong><font color="#6f4e37"> { client.phone } </font></strong></li>
                  <li><font color="#C86428">Email: </font><strong><font color="#6f4e37"> { client.mail } </font></strong></li>
                  <li><font color="#C86428">Artist: </font><strong><font color="#6f4e37"> { client.tracks[0].artist } </font></strong></li>
                  <li><font color="#C86428">Track: </font><strong><font color="#6f4e37"> { client.tracks[0].title } </font></strong></li>
                  <ReactPlayer 
                    url={ audio }
                    controls
                    width='50'
                    height='150'
                    onLoaded={() =>
                    this.setState(currentState => ({
                        loadedVideosCount: loadedVideosCount + 1,
                        currentPlayingIndex:
                          loadedVideosCount + 1 === youtube_urls.length ? 0 : -1,
                      }))
                    }
                    onStart={() => this.emailClient(index)} //<--------
                    onEnded={() =>
                    this.setState(currentState => ({
                        currentPlayingIndex: currentPlayingIndex + 1,
                      }))
                    }
                    playing={index === currentPlayingIndex}
                  />
                </ul></div>
              )
            })
          }
      </div>
    )
  };
};

export default Jukebox;

I'm getting the following error:

POST http://localhost/send-email/1 404 (Not Found)

endpoint:

@task_bp.route('/send-email/<user_id>', methods=['GET','POST'])
def send_email(user_id):
   try:
     #business logic
     send_async_email.delay()

     return jsonify(response_object), 200    
   except (exc.IntegrityError, ValueError):
     db.session.rollback()
     return jsonify(response_object), 400

nginx reverse proxy config:

  location /send-email {
    proxy_pass        http://web:5000;
    proxy_redirect    default;
    proxy_set_header  Upgrade $http_upgrade;
    proxy_set_header  Connection "upgrade";
    proxy_set_header  Host $host;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Host $server_name;
  }

'web' and 'client' services, on which mailing services depend, are built like so using docker-compose:

services:

  web:
    build:
      context: ./services/web
      dockerfile: Dockerfile-dev
    volumes:
      - './services/web:/usr/src/app'

    ports:
      - 5001:5000
    environment:
      - FLASK_ENV=development
      - APP_SETTINGS=project.config.DevelopmentConfig
      - DATABASE_URL=postgres://postgres:postgres@web-db:5432/web_dev 
      - DATABASE_TEST_URL=postgres://postgres:postgres@web-db:5432/web_test
      - SECRET_KEY=my_precious
      - GOOGLE_APPLICATION_CREDENTIALS=/usr/src/app/project/api/resources/youtube/urls/z.json  
    depends_on:  
      - web-db
      - redis

  client:
    build:
      context: ./services/client
      dockerfile: Dockerfile-dev
    volumes:
      - './services/client:/usr/src/app'
      - '/usr/src/app/node_modules'
    ports:
      - 3000:3000
    environment:
      - NODE_ENV=development
      - REACT_APP_WEB_SERVICE_URL=${REACT_APP_WEB_SERVICE_URL}
    depends_on:
      - web

and everytime I build them, I do:

 $export REACT_APP_WEB_SERVICE_URL=http://localhost 

what could be wrong?


NOTE:

If I post directly to the endpoint using POSTMAN to http://localhost:5001/send-email/1, it works.

8-Bit Borges
  • 9,643
  • 29
  • 101
  • 198
  • Have you read the error? The error says your endpoint isn't found, this doesn't seem related to react or react-player. Is your URL missing the port? (like http://localhost:1234/send-mail/1) ? Do you have an endpoint handler that takes in a user ID at the end of it? is auth failing? – Andy Ray Dec 04 '19 at 01:19
  • I use proxy which declares the port `location /send-email { proxy_pass http://web:5000;` – 8-Bit Borges Dec 04 '19 at 01:23
  • 2
    The error says your URL is `http://localhost`, but the endpoint you're showing us is `http://localhost:5001` – Andy Ray Dec 04 '19 at 02:32
  • all other endpoints work with the same config, I don't think thats the point. Maybe `event.preventDefault()`, lke i said on my edit. – 8-Bit Borges Dec 04 '19 at 02:37
  • Probably, there is no any argument of `onStart()` callback, so there is no any `event` that you can call `event.preventDefault()` on. `onStart` is not a browser event, so you can't prevent any default handling. – Vlad DX Dec 06 '19 at 22:01
  • Update your line "const url = `${process.env.REACT_APP_WEB_SERVICE_URL}/send-mail/${userId}`;". Remove `${process.env.REACT_APP_WEB_SERVICE_URL}`. – Vlad DX Dec 06 '19 at 22:03
  • @VladimirSerykh error persists.... – 8-Bit Borges Dec 07 '19 at 02:50
  • Could it be that the call is to `onStart={() => this.emailClient(index)} ` passing in just `index`, but the function `emailClient(event, index){` expects `event` and `index` ? – Bernard Leech Dec 08 '19 at 15:31
  • no..when I pass `index` only, I correct and declare only `index` as argument, remove `event` from `emailClient()` and error persists – 8-Bit Borges Dec 08 '19 at 18:00
  • Where does `proxy_pass http://web:5000` go? – tony Dec 08 '19 at 23:40
  • 3
    Your route is `send-email` while your url is sending request to `send-mail` @@ – junwen-k Dec 08 '19 at 23:45
  • 4
    Browsers send ```OPTIONS``` request to check if the backend server allows cross origin requests or not. For example: if the url you are making an async request to is not in the same domain or not for example http://localhost and http://localhost:5001 are different domains, browser will send an `OPTIONS` request first and post server acknowledging to it then will the browser send the actual post request. Couple of things to check: 1) If the proxy pass is passing the request to the correct upstream server 2) You are using correct endpoint from the front end – Karthik Venkateswaran Dec 10 '19 at 05:52
  • Try also to add a slash at the end of the url in React, like so : ```/send_email/${user.id}/ ```, sometimes backend frameworks expect such a trailing slash on the url. – Emmanuel Meric de Bellefon Dec 11 '19 at 15:09
  • Are you able post directly to the endpoint using POSTMAN to http://localhost/send-email/1? Without the port. – Lutti Coelho Dec 11 '19 at 17:47
  • no it throws error CANNOT POST – 8-Bit Borges Dec 11 '19 at 17:52

2 Answers2

3

Adding port 5001 after localhost: at client fixed it for me:

   const url = `${process.env.REACT_APP_WEB_SERVICE_URL}:5001/send-email/${userId}`;

Console:

XHR finished loading: OPTIONS "http://localhost:5001/send-email/1".

I still don't get why, since all other dozens of endpoints dispense 5001, but it is fixed. Any answer that clarifies the reason will be granted the bounty.

8-Bit Borges
  • 9,643
  • 29
  • 101
  • 198
1

In your Nginx frontend webserver configuration, you set up a reverse proxy to pass request to /send-email to the web application server with hostname web listening on port 5000.

location /send-email {
    proxy_pass        http://web:5000;

This means you can request to /send-email/1 directly.

You noted in your question that an OPTIONS request is performed in your browser and gets a 404 response whereas a POST is what is expected.

The reason for this is that the request performed with the axios client is a preflighted one.

For preflight requests, the browser first makes an OPTIONS request to the path to determine if the request is safe to send.

A number of characteristics determine a preflight request, one of which is the value of Content-Type header being one other than application/x-www-form-urlencoded | multipart/form-data | text/plain

Response to a OPTIONS request to confirm that a POST request is safe must have Access-Control-Allow-* headers which say so. For example, the request headers in your POST request are:

  • Content-Type
  • Authorization

For a confirmation from the server that the POST request is safe, response headers for the OPTIONS request must be

Access-Control-Allow-Origin: http://localhost
Access-Control-Allow-Methods: HEAD, OPTIONS, GET, POST
Access-Control-Allow-Headers: Authorization, Content-Type, Access-Control-Allow-Origin
Access-Control-Max-Age: 86400

You could handle this preflight response in your Nginx configuration. For example

    location /send-email {
        # ...
        if ($request_method = OPTIONS) {
            add_header 'Access-Control-Allow-Origin' 'http://localhost';
            add_header 'Access-Control-Allow-Methods' 'HEAD, OPTIONS, GET, POST';
            add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Access-Control-Allow-Origin';
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Max-Age' 86400;
            return 204;
        }
    }

Alternatively, you could handle OPTIONS request in your view function

from functools import wraps

def cors(f):
    '''Handle preflight OPTIONS request'''
    @wraps
    def wrapper(*args, **kwargs):
        if request.method != 'OPTIONS':
            return make_response(f(*args, **kwargs))

        response = current_app.make_default_options_response()
        access_control_headers = {
            'Access-Control-Allow-Origin': 'http://localhost',
            'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST',
            'Access-Control-Max-Age': '86400',
            'Access-Control-Allow-Credentials': 'true',
            'Access-Control-Allow-Headers': 'Authorization, Content-Type, Access-Control-Allow-Origin',
        }
        response.headers.extend(access_control_headers)
        return response
    return wrapper

@cors
@task_bp.route('/send-email/<user_id>', methods=['GET','POST'])
def send_email(user_id):
    # ...
Oluwafemi Sule
  • 36,144
  • 1
  • 56
  • 81
  • thank you for your answer. I had forgotten to add `'Access-Control-Allow-Origin': true` to the function, it was there all along, even before my hack adding the port. Please refer to my edit at `emailClient()` function. I hope it keeps your answer valid. – 8-Bit Borges Dec 13 '19 at 04:55
  • All headers set in the `POST` request need to be listed in `Access-Control-Allow-Headers` for the `OPTIONS` response. – Oluwafemi Sule Dec 13 '19 at 05:00