erikras / react-redux-universal-hot-example

A starter boilerplate for a universal webapp using express, react, redux, webpack, and react-transform
MIT License
12k stars 2.5k forks source link

API call from client issue #1245

Closed paulinda closed 8 years ago

paulinda commented 8 years ago

I have an action that fires when the user clicks on a control. The action makes an API call to get some data. When it gets to ApiClient, it is trying to make the call on the local system (http://localhost:3000/...). The SERVER flag is off. Since the web service is not on the local system, the call fails. I was able to remove the check for being on the server but then it fails with a CORS error. This is probably not the way to go. This is for the home page and it uses other actions that work fine. This action happens after the page has loaded so it's using client rendering, I think. Still new to this.

What is the correct way to make an API call from the client after render? Can someone explain how the ApiClient works in regard to the client/server?

VSuryaBhargava commented 8 years ago

If I need to make cross domain api calls I normally proxy them through the server.

You can add the below in the src/server.js ( after line 47 )

const crosAPIUrl = 'http://somedomain.com';
const crosProxy = httpProxy.createProxyServer({
  target: crosAPIUrl
});
app.use('/crosapi', (req, res) => {
  crosProxy.web(req, res, {target: crosAPIUrl});
});

You can make the cros domain calls by calling the url localhost:3000/crosapi/

so if you want to call http://somedomain.com/xyz?foo=bar

you need to call http://localhost:3000/crosapi/xyz?foo=bar

arman-mukatov commented 8 years ago

If you want send request to server from client after rendering, have two way:

1) you send requests through a proxy server(/api/*), in this way you maintain a session state of server-to-server.

2) client can make calls directly to the API server. Then you have to deal with CORS stuff on your API server.

You can also read more details here

paulinda commented 8 years ago

Thanks @VSuryaBhargava. I tried the code but keep getting a 404. In my case, my call is http://xxx.azurewebsites.net/api/Price/GetHistoricalValues?id=test Using your code, this gets converted to http://localhost:3000/crosapi/Price/GetHistoricalValues?id=test I also tried changing it to http://localhost:3000/crosapi/api/Price/GetHistoricalValues?id=test. Maybe it's the 'api' in the path?

@arman-mukatov. I made the changes in 2) a while back and it works for page loads. The issue is doing it after page load. Do you have some code for sending requests through the proxy server when running the client code?

VSuryaBhargava commented 8 years ago

@paulinda I have used the below

const crosAPIUrl = 'http://ergast.com/';
const crosProxy = httpProxy.createProxyServer({
  target: crosAPIUrl
});
app.use('/crosapi', (req, res) => {
  crosProxy.web(req, res, {target: crosAPIUrl});
});

this is the original api

http://ergast.com/api/f1/2013/driverStandings.json

and this works

http://localhost:3000/crosapi/api/f1/2013/driverStandings.json

paulinda commented 8 years ago

@VSuryaBhargava, It works with your Url. Now, it's throwing a 500 error so getting closer. It works correctly using Postman so something in my code.

Thanks for the help.

hengbenkeji commented 8 years ago

I ran into this issue recently. The keyword is CORS, which stands for Cross-Origin Resource Sharing - an inherent security device implemented by your browser. The following code is an working example for the setup of the API server. This example cannot be used in production. You just need to choose an advanced plan for storing your express-session data. Refer to the docs of express-session for details.

The setup for CORS of the API server is in the middle. Then you need to add .withCredentials() method in the ApiClient.js when composing the final superagent query. The code is as follows , Diff meaning differences compared with the current version of this repo.

By the way, Postman has the ability to handle the OPTIONS command. Postman being OK does NOT mean your client-side code is OK.

// api.api.js
import express        from 'express';
import session        from 'express-session'

// Diff 
import bodyParser     from 'body-parser';
import config         from '../src/config';
import * as actions   from './actions/index';
import {mapUrl}       from 'utils/url.js';
import PrettyError    from 'pretty-error';
import http           from 'http';
import SocketIo       from 'socket.io';
//Diff
import logger         from 'morgan'

const pretty = new PrettyError();
const app = express();

const server = new http.Server(app);
const io = new SocketIo(server);
io.path('/ws');

app.use(session({
  secret: 'react and redux rule!!!!',
  resave: false,
  saveUninitialized: false,
  cookie: { maxAge: 60000 * 60 * 8}
}));

// Diff
app.use(function printSession(req, res, next){
  console.log('req.session', req.session)
  return next()
})

app.use(bodyParser.json())
app.use(bodyParser.urlencoded({
  extended: true
}))

app.use(logger('dev'))

// Diff
app.use(function(req, res, next) {
  res.header('Access-Control-Allow-Origin', 'http://localhost:8080')
  //res.header('Access-Control-Allow-Origin', 'http://localhost:3000')
  res.header('Access-Control-Allow-Credentials', 'true')
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE')
  res.header('Access-Control-Allow-Headers', 'Content-Type')
  next()
})

app.use((req, res) => {
  const splittedUrlPath = req.url.split('?')[0].split('/').slice(1);
  const {action, params} = mapUrl(actions, splittedUrlPath);

  if (action) {
    action(req, params)
      .then((result) => {
        if (result instanceof Function) {
          result(res);
        } else {
          res.json(result);
        }
      }, (reason) => {
        if (reason && reason.redirect) {
          res.redirect(reason.redirect);
        } else {
          console.error('API ERROR:', pretty.render(reason));
          res.status(reason.status || 500).json(reason);
        }
      });
  } else {
    res.status(404).end('NOT FOUND');
  }
});

const bufferSize = 100;
const messageBuffer = new Array(bufferSize);
let messageIndex = 0;

if (config.apiPort) {
  const runnable = app.listen(config.apiPort, (err) => {
    if (err) {
      console.error(err);
    }
    console.info('----\n==> API is running on port %s', config.apiPort);
    console.info('==> Send requests to http://%s:%s', config.apiHost, config.apiPort);
  });

  io.on('connection', (socket) => {
    socket.emit('news', {msg: `'Hello World!' from server`});

    socket.on('history', () => {
      for (let index = 0; index < bufferSize; index++) {
        const msgNo = (messageIndex + index) % bufferSize;
        const msg = messageBuffer[msgNo];
        if (msg) {
          socket.emit('msg', msg);
        }
      }
    });

    socket.on('msg', (data) => {
      data.id = messageIndex;
      messageBuffer[messageIndex % bufferSize] = data;
      messageIndex++;
      io.emit('msg', data);
    });
  });
  io.listen(runnable);
} else {
  console.error('==>     ERROR: No PORT environment variable has been specified');
}

// src/helpers/ApiClient.js

import superagent from 'superagent';
import config from '../config';

const methods = ['get', 'post', 'put', 'patch', 'del'];

function formatUrl(path) {
  const adjustedPath = path[0] !== '/' ? '/' + path : path;
  if (__SERVER__) {
    // Prepend host and port of the API server to the path.
    return 'http://' + config.apiHost + ':' + config.apiPort + adjustedPath;
  }

  // Prepend `/api` to relative URL, to proxy to API server.
  // Diff
  return 'http://' + config.apiHost + ':' + config.apiPort +  adjustedPath
}

export default class ApiClient {
  constructor(req) {
    methods.forEach((method) =>
      this[method] = (path, { params, data } = {}) => new Promise((resolve, reject) => {
        const request = superagent[method](formatUrl(path));

        // Diff
        request.withCredentials()

        if(method === 'post') {
          request.type('form')
        }

        if (params) {
          request.query(params);
        }

        if (__SERVER__ && req.get('cookie')) {
          request.set('cookie', req.get('cookie'));
        }

        if (data) {
          request.send(data);
        }

        request.end((err, { body } = {}) => err ? reject(body || err) : resolve(body));
      }));
  }
  empty() {}
}
paulinda commented 8 years ago

@sdruipeng I'm not using the API server, I'm calling a web service on Azure.

The issue ended up being the serialization in the web service. It was serializing the data in Xml format instead of JSON. Added code to the service to fix it. No changes needed in the client.

Thanks everyone for the help.