Using a prefix for strapi routes

As a continuation of discussion github #7845

I tried to prefix all strapi routes with /api in order to be able to isolate them in upstream server (for the following example I didn’t isolate anything). So, I modified the server config as follows:

// modified server config
module.exports = ({ env }) => ({
  host: env('HOST', '0.0.0.0'),
  port: env.int('PORT', 1337),
  url: "/api", // <-- added prefix
  admin: {
    auth: {
      secret: env('ADMIN_JWT_SECRET', 'replaced'),
    },
  },
});

Trying to access the root, then the admin panel (the button correctly directed to/api/admin) I got this:

 Project information
┌────────────────────┬──────────────────────────────────────────────────┐
│ Time               │ Sun Oct 11 2020 10:48:14 GMT+0000 (Coordinated … │
│ Launched in        │ 2095 ms                                          │
│ Environment        │ development                                      │
│ Process PID        │ 20488                                            │
│ Version            │ 3.2.3 (node v12.18.3)                            │
│ Edition            │ Community                                        │
└────────────────────┴──────────────────────────────────────────────────┘

 Actions available

To manage your project 🚀, go to the administration panel at:
http://localhost:1337/api/admin

To access the server ⚡️, go to:
http://localhost:1337/api

[2020-10-11T10:48:25.090Z] debug GET index.html (23 ms) 200
[2020-10-11T10:48:25.173Z] debug GET /api/assets/images/logo_login.png (2 ms) 404
[2020-10-11T10:48:25.336Z] debug GET /favicon.ico (0 ms) 200
[2020-10-11T10:48:26.617Z] debug GET /api/admin (3 ms) 200
[2020-10-11T10:48:26.695Z] debug GET /admin/runtime~main.9279ceea.js (4 ms) 404
[2020-10-11T10:48:26.696Z] debug GET /admin/main.081d5ad3.chunk.js (3 ms) 404

Without that /api prefix everything works fine, but with it looks like subresources inside administration panel are stil requested with unprefixed url. I tried to retreive directly the /admin/runtime~main.9279ceea.js url with /api prefix and it works. Did I do something wrong? It looks like things are mixed up here… If I try to isolate the /api route in upstream it gives:

[2020-10-11T11:48:07.765Z] debug GET /admin (2 ms) 404

PS - I just pasted my latest question from github here as advised. This markdown editor is awesome, is it open source?

1 Like

Hi @SorinGFS

With regards to the editor, I would imagine so as Discourse is open source:


Now onto the prefixing issue, I’ll admit this will be slightly complicated, to get a full view of what is required I would suggest you see the Optional Software part of the documentation. In my below explanation I will be referencing Nginx, but any proxy application can apply here including Traefik, Apache, and others that we don’t have guides for.

The short answer is Strapi itself doesn’t prefix anything, and the backend expects the requested routes to not have a prefix (due to how we manage Koa-Router). What each of the optional software guide does is accept these prefixed routes, strip the prefix, and proxy the request to Strapi.

So with Nginx via the following line, in the sub-folder examples:

rewrite ^/api/(.*)$ /$1 break;

All that does is accept incoming requests on example.com/api, strips the /api and passes the request off to Strapi. So taking the example of example.com/api/blog-posts?_sort=created_at:desc what will be sent to Strapi is instead /blog-posts?_sort=created_at:desc

That server.js url key is also used all over the rest of the application to let other parts know what it’s “public” URL is, effectively it doesn’t actually modify the Koa-router (not to be confused with the server.js admin.url which does actually tweak the router for the Admin panel address).

When you set that server.js url key to /api what you are telling it is that Strapi can be accessed publicly at HOST:PORT/api though Strapi is expecting something before it to strip that /api from the actual request. What it does do is set the Admin panel to request the Strapi backend at HOST:PORT/api since it’s client side rendered.

Let me know if that makes sense or if I need to clarify something, to be very specific, there is no way internally to prefix all routes automatically, you would effectively need to modify every single routes.json file, including plugins, with the prefix. that makes things quite complicated, just as us tweaking Koa-router makes the internals quite ugly.

Thank you for explanation. I tried and it doesn’t work. Let me reproduce the steps:

  1. server.js exactly as above
  2. cleared .cache and build folders
  3. rebuild admin panel with npm run build
  4. nginx config exactly as specified in optional software at subfolder unified.
  5. accessing example.com/ , where Open the administration button correctly links to example.com/api/admin url
  6. accessing the administratipn link results like following in console:
 Project information

┌────────────────────┬──────────────────────────────────────────────────┐
│ Time               │ Tue Oct 13 2020 22:24:48 GMT+0000 (Coordinated … │
│ Launched in        │ 2037 ms                                          │
│ Environment        │ development                                      │
│ Process PID        │ 16423                                            │
│ Version            │ 3.2.3 (node v12.18.3)                            │
│ Edition            │ Community                                        │
└────────────────────┴──────────────────────────────────────────────────┘

 Actions available

Welcome back!
To manage your project 🚀, go to the administration panel at:
http://localhost:1337/api/admin

To access the server ⚡️, go to:
http://localhost:1337/api

[2020-10-13T22:25:45.043Z] debug GET index.html (27 ms) 200
[2020-10-13T22:25:45.120Z] debug GET /assets/images/logo_login.png (6 ms) 200
[2020-10-13T22:25:45.281Z] debug GET /favicon.ico (1 ms) 200
[2020-10-13T22:25:52.463Z] debug GET /admin (3 ms) 404

Here you can see that Nginx stripped the /api part from url, but… /admin not to be found!
Suspecting that the api keyword may be a forbidden word I also tried same steps with another word. Same results.

@DMehaffy

Let me tell you why I think this subject is very important one…

In order to retreive data in a website we have 2 options:

  • to make requests to the same FQDN
  • to make requests to different FQDN (subdomains are also included here)

The second option is CORS subject, and involves a greater number of security settings such as CSP headers, FP headers, Access Control headers with preflight requests, therefore a much longer response time. It is also much more difficult to protect against unwanted XSS on backend. So, I think this second scenario as a last resort option.

Now, to be able to use the first scenario we need to be able to isolate a number of routes, like admin panel related routes, api routes, graphql, documentation and generally speaking all the plugin related routes. If we can’t isolate those routes we can’t apply any access control rules on downstream server.

From what I’ve seen, plugins are easy to isolate because they have a correct and unique prefix. The problem occurs with API and the dashboard. Therefore those urls in the server.js MUST both be used … and should work correctly. Initially I tried to add both urls, but after seeing that it didn’t work I told myself to try one step at a time.

Apart from that, I also noticed that if I run npm run start when I access example.com/ that DEVELOPMENT label does not become PRODUCTION, in console too.

When you build the admin panel you also need to include the NODE_ENV setting, so to build the admin in production you need to use NODE_ENV=production npm run build same applies for running Strapi NODE_ENV=production npm run start

@DMehaffy Thank you Derrick for info. As I see for development mode is not necessary to specify the NODE_ENV, is added by default. Anyway, I think is official: isolating the api and dashboard routes by adding specific url in server.js does not work!
I hope someone will take care of this problem as soon as possible.
PS - If you could find some time you could test on a clean installation to confirm the problem.

1 Like

Sure I can spin up a VM, just a key point to note, regarding the optional software configs I did write and test those myself :wink:

But I can spin up a few Digital Ocean Droplets as tests to show you working examples

@DMehaffy Thank you for your quick replay. I dont use Digital Ocean Droplets, I use dedicated servers, I use nginx 1.19 on downstream, and mongoDB on ubuntu 18.04, and IIS 8 and mongoDB on windows. On both systems I got the same problem, and without the url prefix everything works fine. Mean while I tried to rebuild admin panel with the following server.js configuration:

module.exports = ({ env }) => ({
  host: env('HOST', '0.0.0.0'),
  port: env.int('PORT', 1337),
  url:'/api',
});

I eliminated the admin part of it, exactly as documentation page shows, but still with relative path. I know that an absolute url is recommended, but it didn’t sais that relative url doesn’t work. Well, I got the same result.

Here I need to add a mention: in my setup the connection between nginx and strapi goes thru http without s, since the connection is internal and environment is secure. I don’t think this should be a problem since I already told that without that url prefix everything goes fine.

So, more questions rises up:

  • can this problem be related to mongoDB?
  • when you wrote the documentation did you test the connection to the dashboard too, or just the api connection ?
  • did you test using relative urls?

DigitalOcean Droplets are functionally the same things as dedicated servers, I too use dedicated machines for my own personal projects, the example should still apply to you.

Yes an absolute URL will most likely be required in your case.
No the issue shouldn’t be related to the database.
Yes I tested everything about the Strapi application when I wrote the guides and even setup test virtual machines in my local lab and on some public clouds to validate the configurations.

Relative URLs weren’t used as that would generally imply an IP address being used, and unless the Strapi instance is running on the same machine as Nginx they won’t work, in most cases (especially enterprise production instances) the Public IP address is assigned to a gateway firewall or application firewall and not the instance Strapi is running on.

I’m spinning up some droplets (effectively virtual machines) in Digital Ocean right now and I’ll be using one of test domains that I own. strapi.guru

1 Like

As a side point our one-click option on DigitalOcean uses this same system, you can view the code for how we generate the images here:

Alright spun up an example using Ubuntu 20.04:

Root Domain: http://strapi.guru (with a little hi messages :stuck_out_tongue: )
API (Articles): http://strapi.guru/api/articles
Admin Panel: http://strapi.guru/api/admin ( username: test@test.com, password: Test1234!)

I’ve also taken the liberty of symlinking some of the config files into the public directory so you can see them directly:

/etc/nginx/sites-enabled/strapi.conf => http://strapi.guru/api/strapi.conf
/etc/nginx/conf.d/upstream.conf => http://strapi.guru/api/upstream.conf
/srv/strapi/testProxy/config/env/production/server.js => http://strapi.guru/api/server.js

Other than that I did zero other configuration of the droplet, simply installed the dependencies, created a Strapi app, configured Strapi and Nginx, started Strapi with pm2 and reloaded Nginx.

If you want I can test the sub-folder split also instead of just the unified.
Here is a sample of the logs:

And proof of the machine, is what I claim it to be:

1 Like

@DMehaffy Thank you, you were so kind as to explain the whole process in the smallest detail. I got it working with the same absolute url as the one set in nginx as server name. Now I understand better the whole process, there is a script that is being loaded in the browser, and that script should contain the full url used in a fetch call that completes the process. That script is not seen by the browser as belonging to the request host, therefore if url is /api/admin the fetch call goes elsewhere…

This is the point where website fails to complete the process if url is relative or wrong:

o = l.startsWith(o, '/') ? '' + strapi.backendURL + o : o,
i && i.params && (o = o + '?' + ac(i.params)),
i && i.body && u && (i.body = JSON.stringify(i.body)),
fetch(o, i).then(oc).then(rc).then(e=>c ? ic(e)  : e)
}

Basically I don’t see any situation in where relative urls can be used to isolate the routes, so maybe not a bad idea to specify that in documentation.

I did one more test, to check if with this settings can I access the api routes only (without admin panel) from different domain (subdomain). As I expected it works! This means that the absolute url set in server.js does not count for regular api calls. Now I have anything I need, thank you again Derrick.

No problem :slight_smile:

(FYI I’ll shutdown that test server tomorrow)

@DMehaffy Ok.

Hi @DMehaffy

I’ve also taken the liberty of symlinking some of the config files into the public directory so you can see them directly:
/etc/nginx/sites-enabled/strapi.conf
/etc/nginx/conf.d/upstream.conf
/srv/strapi/testProxy/config/env/production/server.js

Could you share it again? Because you’ve shutdown the test server, that I can’t get it.

Thanks.

@toanhq most of those configs directly come from our documentation:

https://strapi.io/documentation/developer-docs/latest/deployment/nginx-proxy.html

If you hit any issues following those docs let me know so I can make a PR to clarify them.

2 Likes

Hi @DMehaffy

Sorry, I missed this document. It’s very useful, now I can do it by myself.
Thanks.

1 Like

then update nginx.conf file

http {
upstream strapi {
    server 127.0.0.1:1337;
}
#tcp_nopush     on;

#keepalive_timeout  0;
keepalive_timeout  65;

#gzip  on;

server {
    listen       8888;
    server_name  localhost;


    location / {
        root C:/projects/bh-service-catalog-svc;
    }
    location /service-catalog/ {
        rewrite ^/service-catalog/(.*) /$1  break;
        proxy_pass http://strapi;
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $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-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_pass_request_headers on;
    }
    location /service-catalog/assets/ {
        rewrite /service-catalog/(.*) /$1  break;
        proxy_pass http://strapi/assets;
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $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-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_pass_request_headers on;
    }
    location /dashboard {
        proxy_pass http://strapi/dashboard;
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $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-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_pass_request_headers on;
    }}


server.js
module.exports = ({ env }) => ({
  host: env('HOST', '0.0.0.0'),
  port: env.int('PORT', 1337),
  url: 'http://localhost:8888/service-catalog',
  admin: {
    url:'http://localhost:8888/dashboard',
    auth: {
      secret: env('ADMIN_JWT_SECRET', 'f8bbc8130154293e157e1e01ab09d62d'),
    },
  },
});

then delete build , .cache folder
then run npm run build
then run npm run develop

I missed this document. It’s very useful, now I can do it by myself.
Thanks


Edit by Mod: Removed advertisement link

Hi,

This results in a 404 error.