Search code examples
javascriptasync-awaittry-catchextract

How to extract logic containing async await and try catch to it's own file in express Node js


Below logic is working. I need it in my server file in React app. How do I extract this (or the most of it) to its own file, like redirectHandler.ts?

if (req.url.includes('foo')) {
    const GET_ID = `query ($url: String!) {
      page (url: $url){
        content {
          ...on Page {
            id
          }
        }
      }
    }`;

    const options = {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        query: GET_ID,
        variables: {
          targetUrl: req.originalUrl,
        },
      }),
    };

    const getData = async () => {
      const response = await fetch(process.env.ENDPOINT, options);
      const json = response.json();
      return json;
    };

    try {
      const result = await getData();
      res.redirect(`/my-page/?id=${result.data.page.content.id}`);
    } catch (err) {
      console.error(err);
    }

    return;
  }

I tried the following:

I create a file redirectHandler.ts:

import * as express from 'express';

import GET_ID from './videoRedirectHandler.gql';

const videoRedirectHandler = async (req: express.Request, res: express.Response) => {
  if (req.url.includes('foo')) {
    const getData = async () => {
      const options = {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          query: GET_ID,
          variables: {
            targetUrl: req.originalUrl,
          },
        }),
      };

      const response = await fetch(process.env.ENDPOINT, options);
      const json = response.json();
      return json;
    };

    try {
      const result = await getData();
      res.redirect(`/my-page/?id=${result.data.page.content.id}`);
    } catch (err) {
      console.error(err);
    }
  }
};

export default videoRedirectHandler;

Then I call it in the server file like: videoRedirectHandler(req, res)

But now I get this error:

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

In the not-extracted example (initial code), I fixed this issue by adding return to the end of the if-block. But in my videoRedirectHandler function it's not possible.

How to fix this?

Update: Thanks to @bergi I created two separate functions, which are working fine:

// getPageData()

import GET_PAGE_DATA from './getPageData.gql';

const getPageData = async (targetUrl: string) => {
  const getData = async () => {
    const options = {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        query: GET_PAGE_DATA,
        variables: {
          targetUrl,
        },
      }),
    };

    const response = await fetch(process.env.ENDPOINT, options);
    const json = response.json();
    return json;
  };

  const result = await getData();

  if (result.errors?.length) {
    throw new Error('Unable to parse json response');
  }
  return result.data.page;
};
export default getPageData;

Inside above function I created another function getData(). Is that necessary?

// getRedirectUrl()

import getPageData from './getPageData';

const getRedirectUrl = async (url: string, originalUrl: string) => {
  if (url.includes('/foo')) {
    const { content } = await getPageData(originalUrl);
    if (content.type === 'MyPage') {
      return `/my-page/?id=${content.id}`;
    }
  }
  return null;
};

export default getRedirectUrl;

Why did you used url and originalUrl here?

Update 2: In the browser when requesting for a non-existing url (but which contains /foo in the url), it gives a blank screen now. In terminal it throws new Error('Something went wrong'); from getPageData() function.

When I inspect result.errors it gives:

[
  {
    message: '404: Not Found',
    extensions: {
      code: 'INTERNAL_SERVER_ERROR',
      serviceName: 'node',
      response: [Object],
      exception: [Object]
    }
  }
]

And in GraphQL playground it gives:

 "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "serviceName": "node",
        "response": {
          "url": "http://fooo/path/etc",
          "status": 404,
          "statusText": "Not Found",
          "body": {
            "message": "No route found for \"GET /fooo/path/etc\""
          }
        },

I think because the GraphQL query works only if the targetUrl is correct. In GraphQL playground it gives the same error when adding a non existing url.

How to fix this issue? It should return to 404 page or something like that and get out of this function. Below both functions:

getPageData():

const getPageData = async (targetUrl: string) => {
  const options = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query: GET_DATA,
      variables: {
        targetUrl,
      },
    }),
  };

  const response = await fetch(process.env.ENDPOINT, options);

  if (!response.ok) {
    throw new Error('Unable to parse json response');
  }

  const result = await response.json();

  if (result.errors?.length) {
    console.log('foo', result.errors);
    throw new Error('Something went wrong');
  }
  return result.data.page;
};

export default getPageData;

And getRedirectUrl():

import getPageData from './getPageData';

const getRedirectUrl = async (url: string) => {
  if (url.includes('/foo')) {
    const { content } = await getPageData(url);
    if (content.type === 'MyPage') {
      return `/my-page/?id=${content.id}`;
    }
  }
  return null;
};

export default getRedirectUrl;

Update 3: When I add return { content: { id: null, type: null } }; inside if statement:

  if (result.errors?.length) {
    // throw new Error('Something went wrong');
    return { content: { id: null, type: null } };
  }

It works fine. Is there a better/cleaner way to solve this issue?

Update 4: I fixed the issue by adding try catch inside getRedirectUrl() function:

const getRedirectUrl = async (url: string) => {
  if (requestUrl.includes('/foo')) {
    try {
      const { content } = await getData(url);

      if (content.type === 'MyPage') {
        return `/my-page/?id=${content.id}`;
      }
    } catch (error) {
      console.error(error);
    }
  }
  return null;
};

Solution

  • Do not extract the if (req.url.includes('foo')) { part, and particularly the early return, into the function as well. Keep that in your original code:

    if (req.url.includes('foo')) {
      videoRedirectHandler(req, res);
      return;
    }
    

    The return keyword is crucial. It prevents the rest of your router handler function from running. You cannot move that into a different function and expect the control flow to be the same.


    Alternatively, you can write a helper function that determines the redirect URL conditionally, and have your routing code use its result:

    async function getPageData(targetUrl) {
      …
      if (result.errors?.length) throw new Error(…);
      return result.data.page;
    }
    async function getRedirectUrl(url, originalUrl) {
      if (req.url.includes('foo')) {
        const {content} = await getPageData(originalUrl);
        if (content.type === 'Bar') {
          return `/my-page/?id=${content.id}`;
        }
      }
    }
    

    These are two functions you can easily unit-test. Then in the server's request handler, write

    const redirectUrl = getRedirectUrl(req.url, req.originalUrl);
    if (redirectUrl) {
      res.redirect(redirectUrl);
      return;
    }
    …