Skip to content

🧱 Intercept requests which are blocked by PerimeterX - pop up the challenge and retry the request

Notifications You must be signed in to change notification settings

fiverr/perimeterx-axios-interceptor

Repository files navigation

perimeterx-axios-interceptor

🧱 Intercept requests which are blocked by PerimeterX - pop up the challenge and retry the request

Using Advanced Blocking Response blocked JSON requests receive a JSON response with status 403. This response includes a payload which allow us to display PerimeterX's challenge. After visitor is exonerated, the original request will be sent and resolved using the original promise.

Playground

Visit the playground to experience the behaviour

Quick implementation

import axios from 'axios';
import { attach } from 'perimeterx-axios-interceptor';

attach(axios);

Implement all the things!

import axios from 'axios';
import { attach, detach } from 'perimeterx-axios-interceptor';

attach(axios, {
    filter: ({ path }) => !/\/logger/.test(path),
    onintercept: request => logger.info(`Intercepted a block response from request ${request.url}`),
    onignore: request => logger.info(`Ignored a block response from request ${request.url}`),
    onsuccess: request => logger.info(`Exonerated a request to ${request.url}`),
    onfailure: (request, error) => logger.info(`Failed to exonerate request to ${request.url}: ${error.message}`),
    onerror: error => logger.error(error),
    simulate: true, // Will **not** load the challenge
    modalConfig: {
        className: 'my-challenge-popup',
        title: 'Are you human?',
        subtitle: 'Please complete the challenge',
        quickfixes: [
            '1. Disable adblocker',
            '2. Enable Javascript'
        ],
        suffix: 'Still having issues? Contact support at support@example.com',
        timeout: 3000,
        allowClose: false
    }
});

// Remove the interceptor for some reason. Perhaps in order to re attach with different settings
detach(axios);

// Disable Auto-ABR
window._pxMonitorAbr = false;

Flow description

Using the feature Advanced Blocking Response featured in PerimeterX's NginX Lua plugin

  1. Request is blocked by PerimeterX (403)
  2. Challenge modal is added to UI
  3. Challenge is resolved by user (user is exonerated)
  4. Replay original request and resolve original promise

HTML output

View full HTML output
<dialog class="perimeterx-async-challenge" open="open">
    <div>
        <p class="title">One Small Step</p>
        <p class="subtitle">Please check the box below to continue your normal visit</p>
        <div id="px-captcha" class="challenge-box">
            <!-- Challange markup (div.g-recaptcha) injected by PerimeterX Javascript -->
        </div>
        <p class="quickfix">Please exclude this website from ad blocking or ad filtering software.</p>
        <p class="quickfix">Make sure you don't have any browser extensions tampering with request headers or user agent string.</p>
        <p>If you're still having trouble accessing the site, please contact customer support.</p>
        <style>
.perimeterx-async-challenge {
    z-index: 10000;
    position: fixed;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    border: 0;
    margin: 0;
    padding: 0;
    background: rgba(0, 0, 0, .3);
    color: black;
}
.perimeterx-async-challenge > div {
    margin: 20vh 20vw 0;
    background: white;
    box-shadow: 0 0 2em rgba(0, 0, 0, .4);
    padding: 1em 1.5em;
}
.perimeterx-async-challenge p,
.perimeterx-async-challenge .challenge-box {
    margin: 0 0 .5em;
}
.perimeterx-async-challenge .title {
    font-size: 2em;
    font-weight: bold;
}
.perimeterx-async-challenge .subtitle {
    font-size: 1.4em;
}
.perimeterx-async-challenge .quickfix {
    font-size: .8em;
    margin: 0;
}
.perimeterx-async-challenge .quickfix:before {
    content: "•";
    margin: 0 .5em
}
@media screen and (max-width:1040px) {
    .perimeterx-async-challenge > div {
        margin: 10vh 10vw 0;
    }
}
@media screen and (max-width:800px) {
    .perimeterx-async-challenge > div {
        margin: 5vw 5vw 0;
    }
}
        </style>
    </div>
</dialog>
<script src="https://captcha.px-cdn.net/<PERIMETERX_APP_IP>/captcha.js"></script>

If you add a custom class, dialog element will include both class names: <dialog class="perimeterx-async-challenge my-challenge-popup" open>

API

1st argument: axios

This is the Axios instance this plugin will intercept. If you use a global axios instance, just pass that one in

2nd argument: options

This is an optional object. All of its properties are also optional:

filter {function}

Filter function is fired before the intercepting function. If filter is passed as an argument, it must return a truthy value for the interceptor to fulfil it's role. Falsy values will result in the interceptor passing the response as is. It's signature includes the following named arguments:

Name Type Meaning Usage
path string Request original path filter: ({ path }) => !/^\/(tracking|beacon)(\/|$)/.test(path)
appId string PerimeterX Application ID filter: ({ appId }) => appId === window._pxAppId

It is considered a good practice not to disrupt the user experience for background communication suck as liveness beacons, logs and metrics. The axios error will be thrown to the listener and will be added a new field, ignored, so that consumers can elect to ignore these skipped blocks.

Requests that will fail the filter method will fire the "onignore" callback from your configuration.

axios('/beacon')
    .catch(
        error => error.ignored
            ? logger.debug('Ignored blocked request')
            : logger.error(error)
    );
onerror {function}

This function is called when an internal error happened with this interceptor The signature includes the error:

onerror: (error) => logger.error(error)
onintercept {function}

This function is called on every time a request is recognised as a PerimeterX block. The signature includes the original request object (axios.config):

onintercept: (request) => logger.info({ message: 'Axios intercepted a PerimeterX block response', url: request.url })
onignore {function}

Similar to onintercept, only this will fire for ignored request that did not pass the filter method. The signature includes the original request object (axios.config):

onignore: (request) => logger.info({ message: 'Axios ignored a PerimeterX block response', url: request.url })
onsuccess {function}

This function is called when a challenge was successfully completed. The signature includes the original request object (axios.config):

onsuccess: (request) => logger.info({ message: 'Axios interceptor exonerated request', url: request.url })
onfailure {function}

This function is called when a challenge was successfully completed. The signature includes the original request object (axios.config) and the rejection error:

onfailure: (request, error) => logger.info({ message: error.message, url: request.url, stack: error.stack })
simulate {boolean}

Set "simulate" to a truthy value to allow monitoring without prompting users with exoneration. The callback onintercept will fire, the rest will not. The promise will be rejected with the PerimeterX 403 response.

modalConfig {object}

This object allows configuration of the modal GUI:

  • className ({string}): Add custom className to modal
    • Default: None
  • title ({string}): Replace or disable default title
    • Default: "One Small Step"
  • subtitle ({string}): Replace or disable default subtitle
    • Default: "Please check the box below to continue your normal visit"]
  • quickfixes ({string[]}): Replace or disable default quick fixes (list)
    • Default:
      • "• Please exclude this website from ad blocking or ad filtering software."
      • "• Make sure you don't have any browser extensions tampering with request headers or user agent string.",
  • suffix ({string}): Replace or disable default suffix
    • Default: "If you're still having trouble accessing the site, please contact customer support."
  • timeout ({number}): Time, in milliseconds, to allow PerimeterX script to load before aborting
    • Default: 3000 (3 seconds)
  • allowClose ({boolean}): Allow users to close the modal
    • Default: true

Setting "title", "subtitle", "quickfixes", or "suffix" to a falsy value (null, empty string...) will prevent them from being rendered to GUI.

modalConfig: {
    className: 'my-challenge-modal',
    title: 'Just a little check',
    quickfixes: [
        '=> Turn off ad blockers',
        '=> Contact support if you need further assistance'
    ],
    suffix: null,
    timeout: 5000
}

About axios instances

If you plan to create axios instances, I suggest you consider using axios-inherit to add interceptor inheritance capability.