Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: fallback path when backend returns 404 #606

Closed
manuel-guilbault opened this issue Nov 27, 2017 · 11 comments
Closed

Feature request: fallback path when backend returns 404 #606

manuel-guilbault opened this issue Nov 27, 2017 · 11 comments
Assignees

Comments

@manuel-guilbault
Copy link

@manuel-guilbault manuel-guilbault commented Nov 27, 2017

I heavily use Azure Functions proxies to serve single page applications stored in a Blob Storage container. However, when those applications use push state-based routing, deep linking doesn't work by default because the web server is expected not to return a 404, but to fall back to index.html (or some other default document) when the requested path is not found. This way, the client app is loaded and its router can notice that the current path is the root directory and can load the property view based on its routing table.

At the moment, I end up copy & pasting the same function acting as proxy with this fallback mechanism in all of my apps. It would be pretty neat if Azure Functions proxies supported this.

@paulbatum
Copy link
Member

@paulbatum paulbatum commented Nov 27, 2017

@safihamid any thoughts on this?

@safihamid
Copy link

@safihamid safihamid commented Nov 27, 2017

@manuel-guilbault could you please provide some examples of what you do today and what you think should be supported?

@safihamid safihamid self-assigned this Nov 27, 2017
@manuel-guilbault
Copy link
Author

@manuel-guilbault manuel-guilbault commented Nov 28, 2017

@safihamid sure :)

Today my code acts just like a proxy. The only difference is that, if the backend returns a 404, my function
drops the 404 response and falls back to sending another request for index.html to the backend. Then it just pipes the response of this second request back to the client.

The new feature I'd like to see is being able to specify fallback paths per HTTP status code for a given proxy. The proxy would then follow this algorithm:

  1. Upon receiving a request, it forwards the request to the backend.
  2. Upon receiving the response from the backend, it checks if its status code matches one of the fallback paths' status code.
    a. If no matching fallback path is found, it just sends the response back to the client, just like it actually does.
    b. If a matching fallback path is found, it drops the response and sends a new request to the matching fallback path, then forwards the new response to the client.

This would allow to use proxies not only to handle deep linking for single page applications, but also to support different response overriding scenarios such as custom error pages.

@safihamid
Copy link

@safihamid safihamid commented Nov 28, 2017

@manuel-guilbault thanks, we will look into it in our planning.

@manuel-guilbault
Copy link
Author

@manuel-guilbault manuel-guilbault commented Nov 29, 2017

@safihamid awesome thanks!

@mathewc
Copy link

@mathewc mathewc commented Feb 20, 2019

Closing this. All future investments will be made in APIM Consumption Tier.

@mathewc mathewc closed this Feb 20, 2019
@Amthieu
Copy link

@Amthieu Amthieu commented Jun 1, 2019

I heavily use Azure Functions proxies to serve single page applications stored in a Blob Storage container.

@manuel-guilbault my setup is the same and I'm having a problem serving my SPA through my proxy. How do you redirect 404 errors to your index.html? My app works fine when GETing the root (https://site.com), but I get 404 errors for any specific page (https://site.com/specific-page).

@safihamid was this feature added? According to the Angular documentation, 404 errors must be redirected to index.html which allows https://site.com/specific-page to work. Some examples are provided to setup a web server to do this, I just can't seem to figure out how to do it with a Function App Proxy within Azure.

TLDR can 404 responses be redirected to index.hmtl?

@manuel-guilbault
Copy link
Author

@manuel-guilbault manuel-guilbault commented Jun 3, 2019

@Amthieu there's a new feature that allows to store static websites on Blog Storage (see this). No need for proxy functions anymore. You can bind custom domains and TLS certificates to the blob storage through an Azure CDN (described here). As for deep linking support, you can simply use your index.html as the Error document path, and you'll get what you need.

@Amthieu
Copy link

@Amthieu Amthieu commented Jun 3, 2019

@manuel-guilbault That's exactly what I've used. My problem is that using index.html as the Error document path returns a 404 status code which is not desired for SEO. That's why I chose to add proxies. The problem is that Azure Function proxies don't support regex and I can't create separate proxies for /build.js (actual file) and /page (page that should be proxied to index.html).

I don't know if this problem is specific to Azure, but I find it weird that SPAs are not better supported in a Storage Account. Is my last option hosting it in a App Service?

@manuel-guilbault
Copy link
Author

@manuel-guilbault manuel-guilbault commented Jun 4, 2019

@Amthieu at the end of the day, if you can't get exactly what you need out of Azure Function Proxies or Blob Storage for static websites, you can always create your own Azure function that will implement exactly the behavior you need. This is what I ended up doing.

@manuel-guilbault
Copy link
Author

@manuel-guilbault manuel-guilbault commented Jun 4, 2019

@Amthieu Here's the Azure function I use most of the time. It first makes sure the request is in HTTPS and, if it isn't, sends a permenant redirect to the HTTPS version of the URL. Then it forwards the request to the underlying Blob Storage and, if the Blob Storage returns a 404, it drops the response and resends a request to the Blob Storage for the index.html.

index.js

const http = require('http');
const path = require('path');
const { parse: parseUrl, format: formatUrl } = require('url');

module.exports = function(context, request) {
  // Make sure received request is on HTTPS. If not, return 301 Moved Permanently to HTTPS version of same URL.
  const originalUrl = parseUrl(request.originalUrl);
  if ((originalUrl.protocol || '').toLowerCase() !== 'https:') {
    const secureUrl = withProtocol(request.originalUrl, 'https:');
    context.log.info(`Received request for ${request.originalUrl}. Redirecting permanently to ${secureUrl}.`);
    context.done(null, {
      status: 301,
      headers: { 'location': secureUrl },
      body: ''
    });
    return;
  }

  // Forward request to Blob Storage. If Blob doesn't exist, fall back to /index.html
  const pathname = context.bindingData.path || '';
  context.log.info(`Received request for path ${pathname}`);
  context.log.verbose(` - StorageContainer.HostAndContainer: ${process.env['StorageContainer.HostAndContainer']}`);
  context.log.verbose(` - StorageContainer.SasToken: ${process.env['StorageContainer.SasToken']}`);

  const backendUrl = getBackendUrl(pathname);
  context.log.info(`Requesting Blob Storage at ${backendUrl.href} ...`);

  sendGet(backendUrl, (error, response) => {
    if (error) {
      context.log.error(`Request failed: ${error}`);
      context.done(null, { status: 500 });
      return;
    }

    context.log.info(`Blob Storage returned ${response.status}`);

    if (response.status === 404) {
      const indexUrl = getBackendUrl('index.html');
      context.log.info(`Falling back on ${indexUrl.href}  ...`);
  
      sendGet(indexUrl, (error, response) => {
        if (error) {
          context.log.error(`Request failed: ${error}`);
          context.done(null, { status: 500 });
          return;
        }
  
        context.log.info(`Blob Storage returned ${response.statusCode}`);
        context.done(null, response);
      });
    } else {
      context.done(null, response);
    }
  });
};

function withProtocol(value, protocol) {
  const result = parseUrl(value);
  result.protocol = protocol;
  return formatUrl(result);
}

function getBackendUrl(pathname) {
  const storageHostAndPath = process.env['StorageContainer.HostAndContainer'];
  const sasToken = process.env['StorageContainer.SasToken'];

  if (pathname === '') {
    pathname = 'index.html';
  }

  const hostAndPath = path.posix.join(storageHostAndPath, pathname);
  const rawUrl = `http://${hostAndPath}${sasToken}`;
  return parseUrl(rawUrl);
}

function sendGet(uri, callback) {
  const options = {
    protocol: uri.protocol,
    hostname: uri.hostname,
    port: uri.port,
    path: uri.path,
  };
  const request = http.get(options, (response) => { toProxyResponse(response, callback); });
  request.on('error', (e) => { callback(e, null); });
  request.end();
}

function toProxyResponse(response, callback) {
  const proxyResponse = {
    status: response.statusCode,
    headers: response.headers,
    body: ''
  };
  response.on('error', (error) => { callback(error, null); });
  response.on('data', (chunk) => {
    proxyResponse.body += chunk;
  });
  response.on('end', () => {
    callback(null, proxyResponse);
  });
}

function.json

{
  "bindings": [
    {
      "name": "request",
      "type": "httpTrigger",
      "direction": "in",
      "authLevel": "anonymous",
      "methods": [ "get" ],
      "route": "{*path}"
    },
    {
      "name": "$return",
      "type": "http",
      "direction": "out"
    }
  ]
}

It expects the following AppSettings:

Name Description
StorageContainer.HostAndContainer The host name & container path of the Blob Storage where the static side is stored. E.g.: my-super-static-site.blob.core.windows.net/my-container
StorageContainer.SasToken The SAS token giving read access to the Blob Storage. Can be omitted if the container is public.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
5 participants