Really common issues integrating from front-end

CORS + Preflight Request

Really common issues integrating from front-end

When we talk about client-side applications, advantages of microservices and micro-frontend, integrations from the client-side, and make our apps more independent we are talking about these common issues lets describe them briefly.

CORS(cross-origin resource sharing) for security communicational reasons browsers restrict cross-domain requests, but what is a cross-domain request? imagine a web served from an URL https://my.web.com That ask resources via XMLHttpRequest from another like this https://other.web.net/some-resource.json can be made via GET, POST, PUT, or PATCH methods.

This is a cross-domain request because those are separated domains this could be owned by different developers/organizations and because of that, the browser need to ensure that we can access these resources securely.

Here is where CORS came in help every resource that must be fetched via the Fetch API or XMLHttpRequest must come with extra headers the main header that allows us to consume resources is Access-Control-Allow-Origin this header can be set to allow any origin to consume our resources like this:

Access-Control-Allow-Origin: *

Or only restrict it to some site like this:

Access-Control-Allow-Origin: https://my.web.com

With that, we restrict access to our resources consumed directly from the browser but make it happen in all request is sometimes expensive, imagine that you have to upload via post some big image and after you send all this big request you get noticed that your URL is not allowed to consume this service, and there is where Preflight Requests came to help they are intimately related with cors because when we need access to an external resource like this from our website, the browser sends a request with the method OPTIONS to the server before the main one is sent to verify if this request can be done, normally it verifies if the request method is accepted and if the origin header is present in this resource, with this information and before sending any payload to the browser can check the health status and the capability of the service to execute the request.

This kind of request is called Preflight Request and is automatically called directly from the browser, not by the front end code but the browser itself makes the request to optimize the client resources (avoiding the expensive call of an API if it doesn't have security access or is down).

To finalize here is a graphic representing the way that said when the prefight request is called from Wikipedia

image.png

Here is a postman preflight test to validate if the response requires it and if the response of the OPTIONS request is ok


(function () {
  const request = pm.request
  const url = request.url.toString()
  const requestMethod = request.method
  const headers = request.headers.toObject()
  const origin = headers.origin

  if (!origin) {
    console.error('The request must have an Origin header to attempt a preflight please add it to test the preflight request')
    return
  }

  delete headers.origin

  const requestHeaders = Object.keys(headers).join(', ')

  if (!['GET', 'HEAD', 'POST'].includes(requestMethod)) {
    console.warn(`The request uses ${requestMethod}, so a preflight will be required`)
  } else if (requestHeaders) {
    console.warn(`The request has custom headers, so a preflight will be required: ${requestHeaders}`)
  } else {
    console.info("A preflight may not be required for this request but we'll attempt it anyway")
  }

  const preflightHeaders = {
    Origin: origin,
    'Access-Control-Request-Method': requestMethod
  }

  if (requestHeaders) {
    preflightHeaders['Access-Control-Request-Headers'] = requestHeaders
  }

  pm.sendRequest({
    url,
    method: 'OPTIONS',
    header: preflightHeaders
  }, (err, response) => {
    if (err) {
      throw new Error('Error:', err)
    }
    if(response.code===200){
        console.info(`Preflight response has status code ${response.code}`)
    }else{
        console.error(`Preflight response has status code ${response.code}`)
    }
    console.warn(`Relevant preflight response headers:`)

    const corsHeaders = [
      'access-control-allow-credentials',
      'access-control-max-age'
    ]
    let errors=[];
    const originHeader=response.headers.find(header=>header.key.toLowerCase()==='access-control-allow-origin');
    if(originHeader.value!=='*' && !originHeader.value.includes(origin)){
        errors.push('Cors "access-control-allow-origin" must be * or include '+origin)
        console.error('error on "access-control-allow-origin" header');
    }
    const methodsHeader=response.headers.find(header=>header.key.toLowerCase()==='access-control-allow-methods');
    if(!methodsHeader.value.split(', ').includes(requestMethod)){
        errors.push('Cors "access-control-allow-methods" must  include '+ requestMethod)
        console.error('error on "access-control-allow-methods" header');
    }
    const allowHeaders=response.headers.find(header=>header.key.toLowerCase()==='access-control-allow-headers');
    const allowHedersArray=allowHeaders.value.split(', ');
    const headersKeys=Object.keys(headers);
    const headerDiff=headersKeys.filter(key=>!allowHedersArray.includes(key))
    const acceptedHeaders=allowHedersArray.concat(headerDiff)
    if(headerDiff.length>=1){
        errors.push('Cors "access-control-allow-headers" is "'+ allowHedersArray.join(', ')+'" and should be "' + acceptedHeaders.join(', ')+'"')
        console.error('error on "access-control-allow-headers" header');
    }
    response.headers.each(header => {
      if (corsHeaders.includes(header.key.toLowerCase())) {
        console.info(`please check if this headers are ok - ${header}`)
      }
    })
    if(errors.length){
        throw new Error(errors);
    }
  })
})()