0
votes

I have a production set up as follows;

  • Django REST Framework backend
  • Vuejs frontend
  • Docker container that builds Vuejs for production and copies to Django Docker container /static/ and /template/ folders
  • nginx reverse proxy to handle incoming requests

Everything works fine when I navigate to home page (no backend API calls on homepage) and then navigate around the SPA using the navigation drawer.

I start to get problems when I try to go directly to a Page in the SPA. The backend requests that should fire on "create" in Vuejs are not firing.

I have seen some people suggest this is related to Vue router being in history mode, which I would like to keep.

The main suggested remedy is to add try_files $uri $uri/ /index.html; as a catch all to the nginx config. However, as I am simply proxying all requests to Django to handle the initial stage of routing, and I already have a catch all in my urls.py file (re_path(r"^.*/$", TemplateView.as_view(template_name="index.html"), name="frontend")) then I think I have this covered.

Why would the API requests (which are fired on create of Vuejs page) work when navigating using the router, but not when navigating directly to the Page by URL?

nginx config

server {
    listen 80;
    location / {
        proxy_pass http://csgs:8000;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        # try_files $uri $uri/ /index.html;
    }

    location /static/ {
        alias /home/app/web/static/;
    }
}

django urls.py patterns

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api-auth/", include("rest_framework.urls")),
    path("api/", include("api.urls")),
    path("", TemplateView.as_view(template_name="index.html"), name="home"),
    re_path(
        r"^.*/$", TemplateView.as_view(template_name="index.html"), name="frontend"
    ),
]

SPA index page body:

<body>
  <noscript>
    <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
      Please enable it to continue.</strong>
  </noscript>
  <div id="app"></div>
  <!-- built files will be auto injected -->
</body>

Update: I've put debug statements in and can now compare the request sent and response received in each scenario.

Vue router navigation:

url = api/booking/ground-stations/ user.service.js:11:16
request = {
  "params": null,
  "headers": {
    "Authorization": "Token 164d5d7bc0fc0be90168739958c0c8640ed52f60"
  }
}
response = {
  "data": [
    {}
  ],
  "status": 200,
  "statusText": "OK",
  "headers": {
    "allow": "GET, HEAD, OPTIONS",
    "connection": "keep-alive",
    "content-length": "82",
    "content-type": "application/json",
    "date": "Sat, 10 Oct 2020 21:49:58 GMT",
    "referrer-policy": "same-origin",
    "server": "nginx/1.17.10",
    "vary": "Accept",
    "x-content-type-options": "nosniff",
    "x-frame-options": "DENY"
  },
  "config": {
    "url": "api/booking/ground-stations/",
    "method": "get",
    "headers": {
      "Accept": "application/json, text/plain, */*",
      "Authorization": "Token 164d5d7bc0fc0be90168739958c0c8640ed52f60"
    },
    "params": null,
    "transformRequest": [
      null
    ],
    "transformResponse": [
      null
    ],
    "timeout": 0,
    "xsrfCookieName": "XSRF-TOKEN",
    "xsrfHeaderName": "X-XSRF-TOKEN",
    "maxContentLength": -1,
    "maxBodyLength": -1
  },
  "request": {}
}

Browser refresh:

url = api/booking/ground-stations/ user.service.js:11:16
request = {
  "params": null,
  "headers": {
    "Authorization": "Token 164d5d7bc0fc0be90168739958c0c8640ed52f60"
  }
}
response = {
  "data": "<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content=\"IE=edge\"><meta name=viewport content=\"width=device-width,initial-scale=1\"><link rel=icon href=/static/favicon.ico><title>CS: GS</title><link rel=stylesheet href=\"https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900\"><link rel=stylesheet href=https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css><link rel=stylesheet href=https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css><link href=\"https://fonts.googleapis.com/css2?family=Orbitron:wght@400;800&display=swap\" rel=stylesheet><link href=/static/css/app.d8cde755.css rel=preload as=style><link href=/static/css/chunk-vendors.93ac251e.css rel=preload as=style><link href=/static/js/app.7e344e2e.js rel=preload as=script><link href=/static/js/chunk-vendors.945fac67.js rel=preload as=script><link href=/static/css/chunk-vendors.93ac251e.css rel=stylesheet><link href=/static/css/app.d8cde755.css rel=stylesheet></head><body><noscript><strong>We're sorry but frontend doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/static/js/chunk-vendors.945fac67.js></script><script src=/static/js/app.7e344e2e.js></script></body></html>",
  "status": 200,
  "statusText": "OK",
  "headers": {
    "connection": "keep-alive",
    "content-length": "1315",
    "content-type": "text/html; charset=utf-8",
    "date": "Sat, 10 Oct 2020 21:51:25 GMT",
    "referrer-policy": "same-origin",
    "server": "nginx/1.17.10",
    "x-content-type-options": "nosniff",
    "x-frame-options": "DENY"
  },
  "config": {
    "url": "api/booking/ground-stations/",
    "method": "get",
    "headers": {
      "Accept": "application/json, text/plain, */*",
      "Authorization": "Token 164d5d7bc0fc0be90168739958c0c8640ed52f60"
    },
    "params": null,
    "transformRequest": [
      null
    ],
    "transformResponse": [
      null
    ],
    "timeout": 0,
    "xsrfCookieName": "XSRF-TOKEN",
    "xsrfHeaderName": "X-XSRF-TOKEN",
    "maxContentLength": -1,
    "maxBodyLength": -1
  },
  "request": {...}
}

So my question now becomes; why does refreshing the page result in an html response to my backend API queries when the url and request remain the same for both vuejs router and firefox browser navigation?

UPDATE 2: So did some more investigating. First I removed the catch all statement in Django urls and replaced with explicit statements;

path("", TemplateView.as_view(template_name="index.html"), name="home"),
    path("ground-stations/", TemplateView.as_view(template_name="index.html"), name="gs"),

Now I should see when any 404s occur.

Now when I send a request I can see that when navigating using the Vuejs API axios appends the endpoints to the base url "http://127.0.0.1:7100/", which is correct. However, if I refresh the page the base url now becomes "http://127.0.0.1/ground-stations/", and the endpoints are appended onto this, which is incorrect.

Why is this happening and how do I fix it?

1

1 Answers

1
votes

It seems trying to handle front and backend routes through django is not the best way to go about it.

I found this link useful: https://medium.com/@zhzhang.4390/vue-in-production-how-to-call-or-proxy-apis-f8045b5a7d16

Basically, I could use some functionality in Vue.js to proxy API calls to the backend, which is what I do for the Docker development deployment. However this didn't seem very elegant to me.

Instead I chose his second option of splitting the backend and frontend into different containers and letting nginx do the routing.

This had the added benefit of completely decoupling my Django backend from the Vue.js front end. Now Django does not need to know about any front end routes or urls and does not need to collect static files from the front end dist directory.

urls.py

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api-auth/", include("rest_framework.urls")),
    path("api/", include("api.urls")),
]

frontend Dockerfile

FROM node:lts-alpine as build-stage

WORKDIR /app
COPY frontend/package*.json ./
RUN npm install
COPY frontend/ .
RUN npm run build

FROM nginx:stable-alpine
COPY --from=build-stage /app/dist /home/frontend
RUN rm /etc/nginx/conf.d/default.conf
COPY deploy/prod/nginx.conf /etc/nginx/conf.d

nginx.conf

server {
    listen 80;
    location / {
      root /home/frontend;
      index index.html;
      try_files $uri $uri/ /index.html;
    }
    location /api/ {
        proxy_pass http://csgs:8000;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
    }
    location /admin {
        proxy_pass http://csgs:8000;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
    }
    location /static/ {
        root /home/frontend;
        try_files $uri $uri/ @csgs_static;
    } 
    location @csmoc_static {
        root /home/csgs;
    } 
}

And I used a Docker Compose file to mount the static folder from my backend Docker container to /home/csgs.

This feels much more like a setup suitable for production than I had it before, and now I can refresh a page and everything works as expected.