Our Partner & Developer boards on the community are moving to a brand new home: the .dev community forums! While you can still access past discussions here, for all your future app and storefront building questions, head over to the new forums.

How to call server API from a Page getInitialProps in my Shopify Embedded App using Node, Next.js

Solved

How to call server API from a Page getInitialProps in my Shopify Embedded App using Node, Next.js

egillanton
Shopify Partner
11 2 4

Introduction: 

I am new to using Node. I have followed the Guide: https://shopify.dev/tutorials/build-a-shopify-app-with-node-and-react

I used the Shopify CLI to create a Node app

I am having problems calling my own API route from my index.js page. I am making a get request to an URL similar to https://123456789.ngrok.io/api/stores/my-development-shop.myshopify.com

I get the following response: Error: Request failed with status code 400 - Bad Request due to somehow my client iframe page that is rendered by  index.js has not authenticated.

How do I authenticate my page so I can call my api?

 

Note: I am able to retrieve the data by typing in my authenticated browser window: 

https://my-develop-store.myshopify.com/admin/apps/APP-ID/api/stores/my-develop-store.myshopify.com  

 

I even tried adding the Cookies from the ctx into the header of my request.

Error:

Error: Request failed with status code 400
...
{
  config: {
    url: 'https://123456789.ngrok.io/api/stores/my-develop-store.myshopify.com',
    method: 'get',
    headers: {
      Accept: 'application/json',
      Cookies: 'shopifyNonce=160078696775500;shopifyNonce.sig=5FoocFlgMKZ2YrgmhBqgSN-o0Eg;shopOrigin=my-develop-store.myshopify.com;shopOrigin.sig=6reeQR8RvnjRchwYFuG-kOl480Q;koa:sess=eyJzaG9wIjoicGF5ZGF5LWRldmVsb3AubXlzaG9waWZ5LmNvbSIsImFjY2Vzc1Rva2VuIjoic2hwYXRfOTI1YjExMjdlNWY1NTU3ODNhNWU5N2M5MDJhYjcwNTAiLCJfZXhwaXJlIjoxNjAwODczMzcwMDY0LCJfbWF4QWdlIjo4NjQwMDAwMH0=;koa:sess.sig=1d2AeB5GMmoEBdwD5bB_0MHU8HM',
      'User-Agent': 'axios/0.20.0'
    },
    transformRequest: [ [Function: transformRequest] ],
    transformResponse: [ [Function: transformResponse] ],
    timeout: 0,
    adapter: [Function: httpAdapter],
    xsrfCookieName: 'XSRF-TOKEN',
    xsrfHeaderName: 'X-XSRF-TOKEN',
    maxContentLength: -1,
    maxBodyLength: -1,
    validateStatus: [Function: validateStatus],
    data: undefined
  },
  request: <ref *1> ClientRequest {
    _events: [Object: null prototype] {
      socket: [Function (anonymous)],
      abort: [Function (anonymous)],
      aborted: [Function (anonymous)],
      connect: [Function (anonymous)],
      error: [Function (anonymous)],
      timeout: [Function (anonymous)],
      prefinish: [Function: requestOnPrefinish]
    },
    _eventsCount: 7,
    _maxListeners: undefined,
    outputData: [],
    outputSize: 0,
    writable: true,
    destroyed: false,
    _last: true,
    chunkedEncoding: false,
    shouldKeepAlive: false,
    useChunkedEncodingByDefault: false,
    sendDate: false,
    _removedConnection: false,
    _removedContLen: false,
    _removedTE: false,
    _contentLength: 0,
    _hasBody: true,
    _trailer: '',
    finished: true,
    _headerSent: true,
    socket: TLSSocket {
      _tlsOptions: [Object],
      _secureEstablished: true,
      _securePending: false,
      _newSessionPending: false,
      _controlReleased: true,
      secureConnecting: false,
      _SNICallback: null,
      servername: '123456789.ngrok.io',
      alpnProtocol: false,
      authorized: true,
      authorizationError: null,
      encrypted: true,
      _events: [Object: null prototype],
      _eventsCount: 10,
      connecting: false,
      _hadError: false,
      _parent: null,
      _host: '123456789.ngrok.io',
      _readableState: [ReadableState],
      _maxListeners: undefined,
      _writableState: [WritableState],
      allowHalfOpen: false,
      _sockname: null,
      _pendingData: null,
      _pendingEncoding: '',
      server: undefined,
      _server: null,
      ssl: [TLSWrap],
      _requestCert: true,
      _rejectUnauthorized: true,
      parser: null,
      _httpMessage: [Circular *1],
      [Symbol(res)]: [TLSWrap],
      [Symbol(verified)]: true,
      [Symbol(pendingSession)]: null,
      [Symbol(async_id_symbol)]: 135897,
      [Symbol(kHandle)]: [TLSWrap],
      [Symbol(kSetNoDelay)]: false,
      [Symbol(lastWriteQueueSize)]: 0,
      [Symbol(timeout)]: null,
      [Symbol(kBuffer)]: null,
      [Symbol(kBufferCb)]: null,
      [Symbol(kBufferGen)]: null,
      [Symbol(kCapture)]: false,
      [Symbol(kBytesRead)]: 0,
      [Symbol(kBytesWritten)]: 0,
      [Symbol(connect-options)]: [Object]
    },
    _header: 'GET /auth HTTP/1.1\r\n' +
      'Accept: application/json\r\n' +
      'Cookies: shopifyNonce=160078696775500;shopifyNonce.sig=5FoocFlgMKZ2YrgmhBqgSN-o0Eg;shopOrigin=my-develop-store.myshopify.com;shopOrigin.sig=6reeQR8RvnjRchwYFuG-kOl480Q;koa:sess=eyJzaG9wIjoicGF5ZGF5LWRldmVsb3AubXlzaG9waWZ5LmNvbSIsImFjY2Vzc1Rva2VuIjoic2hwYXRfOTI1YjExMjdlNWY1NTU3ODNhNWU5N2M5MDJhYjcwNTAiLCJfZXhwaXJlIjoxNjAwODczMzcwMDY0LCJfbWF4QWdlIjo4NjQwMDAwMH0=;koa:sess.sig=1d2AeB5GMmoEBdwD5bB_0MHU8HM\r\n' +
      'User-Agent: axios/0.20.0\r\n' +
      'Host: 123456789.ngrok.io\r\n' +
      'Connection: close\r\n' +
      '\r\n',
    _onPendingData: [Function: noopPendingOutput],
    agent: Agent {
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      defaultPort: 443,
      protocol: 'https:',
      options: [Object],
      requests: {},
      sockets: [Object],
      freeSockets: {},
      keepAliveMsecs: 1000,
      keepAlive: false,
      maxSockets: Infinity,
      maxFreeSockets: 256,
      scheduling: 'fifo',
      maxTotalSockets: Infinity,
      totalSocketCount: 1,
      maxCachedSessions: 100,
      _sessionCache: [Object],
      [Symbol(kCapture)]: false
    },
    socketPath: undefined,
    method: 'GET',
    maxHeaderSize: undefined,
    insecureHTTPParser: undefined,
    path: '/auth',
    _ended: true,
    res: IncomingMessage {
      _readableState: [ReadableState],
      _events: [Object: null prototype],
      _eventsCount: 3,
      _maxListeners: undefined,
      socket: [TLSSocket],
      httpVersionMajor: 1,
      httpVersionMinor: 1,
      httpVersion: '1.1',
      complete: true,
      headers: [Object],
      rawHeaders: [Array],
      trailers: {},
      rawTrailers: [],
      aborted: false,
      upgrade: false,
      url: '',
      method: null,
      statusCode: 400,
      statusMessage: 'Bad Request',
      client: [TLSSocket],
      _consuming: false,
      _dumped: false,
      req: [Circular *1],
      responseUrl: 'https://123456789.ngrok.io/auth',
      redirects: [],
      [Symbol(kCapture)]: false
    },
    aborted: false,
    timeoutCb: null,
    upgradeOrConnect: false,
    parser: null,
    maxHeadersCount: null,
    reusedSocket: false,
    host: '123456789.ngrok.io',
    protocol: 'https:',
    _redirectable: Writable {
      _writableState: [WritableState],
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      _options: [Object],
      _ended: true,
      _ending: true,
      _redirectCount: 1,
      _redirects: [],
      _requestBodyLength: 0,
      _requestBodyBuffers: [],
      _onNativeResponse: [Function (anonymous)],
      _currentRequest: [Circular *1],
      _currentUrl: 'https://123456789.ngrok.io/auth',
      _isRedirect: true,
      [Symbol(kCapture)]: false
    },
    [Symbol(kCapture)]: false,
    [Symbol(kNeedDrain)]: false,
    [Symbol(corked)]: 0,
    [Symbol(kOutHeaders)]: [Object: null prototype] {
      accept: [Array],
      cookies: [Array],
      'user-agent': [Array],
      host: [Array]
    }
  },
  response: {
    status: 400,
    statusText: 'Bad Request',
    headers: {
      'content-type': 'text/plain; charset=utf-8',
      'content-length': '37',
      date: 'Tue, 22 Sep 2020 15:03:10 GMT',
      connection: 'close'
    },
    config: {
      url: 'https://123456789.ngrok.io/api/stores/my-develop-store.myshopify.com',
      method: 'get',
      headers: [Object],
      transformRequest: [Array],
      transformResponse: [Array],
      timeout: 0,
      adapter: [Function: httpAdapter],
      xsrfCookieName: 'XSRF-TOKEN',
      xsrfHeaderName: 'X-XSRF-TOKEN',
      maxContentLength: -1,
      maxBodyLength: -1,
      validateStatus: [Function: validateStatus],
      data: undefined
    },
    request: <ref *1> ClientRequest {
      _events: [Object: null prototype],
      _eventsCount: 7,
      _maxListeners: undefined,
      outputData: [],
      outputSize: 0,
      writable: true,
      destroyed: false,
      _last: true,
      chunkedEncoding: false,
      shouldKeepAlive: false,
      useChunkedEncodingByDefault: false,
      sendDate: false,
      _removedConnection: false,
      _removedContLen: false,
      _removedTE: false,
      _contentLength: 0,
      _hasBody: true,
      _trailer: '',
      finished: true,
      _headerSent: true,
      socket: [TLSSocket],
      _header: 'GET /auth HTTP/1.1\r\n' +
        'Accept: application/json\r\n' +
        'Cookies: shopifyNonce=160078696775500;shopifyNonce.sig=5FoocFlgMKZ2YrgmhBqgSN-o0Eg;shopOrigin=my-develop-store.myshopify.com;shopOrigin.sig=6reeQR8RvnjRchwYFuG-kOl480Q;koa:sess=eyJzaG9wIjoicGF5ZGF5LWRldmVsb3AubXlzaG9waWZ5LmNvbSIsImFjY2Vzc1Rva2VuIjoic2hwYXRfOTI1YjExMjdlNWY1NTU3ODNhNWU5N2M5MDJhYjcwNTAiLCJfZXhwaXJlIjoxNjAwODczMzcwMDY0LCJfbWF4QWdlIjo4NjQwMDAwMH0=;koa:sess.sig=1d2AeB5GMmoEBdwD5bB_0MHU8HM\r\n' +
        'User-Agent: axios/0.20.0\r\n' +
        'Host: 123456789.ngrok.io\r\n' +
        'Connection: close\r\n' +
        '\r\n',
      _onPendingData: [Function: noopPendingOutput],
      agent: [Agent],
      socketPath: undefined,
      method: 'GET',
      maxHeaderSize: undefined,
      insecureHTTPParser: undefined,
      path: '/auth',
      _ended: true,
      res: [IncomingMessage],
      aborted: false,
      timeoutCb: null,
      upgradeOrConnect: false,
      parser: null,
      maxHeadersCount: null,
      reusedSocket: false,
      host: '123456789.ngrok.io',
      protocol: 'https:',
      _redirectable: [Writable],
      [Symbol(kCapture)]: false,
      [Symbol(kNeedDrain)]: false,
      [Symbol(corked)]: 0,
      [Symbol(kOutHeaders)]: [Object: null prototype]
    },
    data: 'Expected a valid shop query parameter'
  },
  isAxiosError: true,
  toJSON: [Function: toJSON]
}

 

 

Files:

server/server.js

 

 

 

 

import "@babel/polyfill";
import dotenv from "dotenv";
import "isomorphic-fetch";
import createShopifyAuth, { verifyRequest } from "@shopify/koa-shopify-auth";
import graphQLProxy, { ApiVersion } from "@shopify/koa-shopify-graphql-proxy";
import Koa from "koa";
import next from "next";
import Router from "koa-router";
import session from "koa-session";
import * as handlers from "./handlers/index";
import { receiveWebhook } from "@shopify/koa-shopify-webhooks";
import { orderPaid } from "../webhooks/orders/paid";
import { customerDataRequest } from "../webhooks/customers/data-request";
import { customerRedact } from "../webhooks/customers/redact";
import { shopRedact } from "../webhooks/shops/redact";
import { getStoreInfoById } from "../api/stores/store";
import fetch from "isomorphic-fetch";

dotenv.config();
const port = parseInt(process.env.PORT, 10) || 8081;
const dev = process.env.NODE_ENV !== "production";
const app = next({
  dev,
});
const handle = app.getRequestHandler();
const { SHOPIFY_API_SECRET, SHOPIFY_API_KEY, SCOPES, HOST } = process.env;
app.prepare().then(() => {
  const server = new Koa();
  const router = new Router();
  const webhook = receiveWebhook({
    secret: SHOPIFY_API_SECRET,
  });

  server.use(
    session(
      {
        sameSite: "none",
        secure: true,
      },
      server
    )
  );
  server.keys = [SHOPIFY_API_SECRET];
  server.use(
    createShopifyAuth({
      apiKey: SHOPIFY_API_KEY,
      secret: SHOPIFY_API_SECRET,
      scopes: [SCOPES],

      async afterAuth(ctx) {
        //Auth token and shop available in session
        //Redirect to shop upon auth
        const { shop, accessToken } = ctx.session;
        await handlers.registerWebhooks(
          shop,
          accessToken,
          "ORDERS_PAID",
          "/webhooks/orders/paid",
          ApiVersion.October19
        );

        ctx.cookies.set("shopOrigin", shop, {
          httpOnly: false,
          secure: true,
          sameSite: "none",
        });
        ctx.redirect("/");
      },
    })
  );
  server.use(
    graphQLProxy({
      version: ApiVersion.October19,
    })
  );

  router.get("/", verifyRequest(), async (ctx) => {
    if (typeof ctx.query.shop !== "undefined") {
      await app.render(ctx.req, ctx.res, "/index", ctx.query);
      ctx.respond = true;
      ctx.res.statusCode = 200;
    }
  });

  router.get("/api/stores/:store", verifyRequest(), async (ctx) => {
    // example: /api/stores/my-develop-store.myshopify.com
    // Check if the request is made by the right store
    console.log("Route called: /api/stores/:store");
    const { shop, accessToken } = ctx.session;
    if (ctx.params.store === shop) {
      await getStoreInfoById(ctx)
        .then((storeInfo) => {
          if (typeof storeInfo !== "undefined") {
            ctx.body = JSON.parse(JSON.stringify(storeInfo));
            ctx.set('Content-Type', 'application/json');
            ctx.respond = true;
            ctx.res.statusCode = 200;
          }
        })
        .catch((err) => {
          ctx.res.statusCode = 500;
          console.log(err);
        });
    } else {
      ctx.res.statusCode = 403;
    }
  });

  router.post("/webhooks/orders/paid", webhook, (ctx) => {
    orderPaid(ctx.state.webhook);
    ctx.res.statusCode = 200;
  });

  router.post("/webhooks/customers/data_request", webhook, (ctx) => {
    customerDataRequest(ctx.state.webhook);
    ctx.res.statusCode = 200;
  });

  router.post("/webhooks/customers/redact", webhook, (ctx) => {
    customerRedact(ctx.state.webhook);
    ctx.res.statusCode = 200;
  });

  router.post("/webhooks/shops/redact", webhook, (ctx) => {
    shopRedact(ctx.state.webhook);
    ctx.res.statusCode = 200;
  });

  router.get("*", verifyRequest(), async (ctx) => {
    await handle(ctx.req, ctx.res);
    ctx.respond = false;
    ctx.res.statusCode = 200;
  });
  server.use(router.allowedMethods());
  server.use(router.routes());
  server.listen(port, () => {
    console.log(`> Ready on http://localhost:${port}`);
  });
});

 

 

 

 

 

pages/index.js

 

 

 

 

class HomePage extends React.Component {
  // Prepares the props sent to the constructor
  // Server sends the ctx.query as the parameter
  static async getInitialProps(ctx_query) {
    const host = ctx_query.req.headers.host;
    const shop = ctx_query.query.shop;
    // If executed on Server Side
    if (typeof shop === "undefined" || typeof host === "undefined") {
      return {};
    }
    // console.log("host:", host);
    // console.log("shop:", shop);
    // console.log(ctx_query);

    let props = {
      shop: shop,
    };
    // const url_query = ctx_query.asPath.substring(1);
    console.log(url_query);
    const url = "https://" + host + '/api/stores/' + shop;

    console.log("Endpoint URL:", url);
    console.log("");
    axios.get(url, { headers: { 'Accept': 'application/json' } })
      .then((response) => {
        console.log("Response Successful")
        if (typeof response.data !== "undefined") {
          console.log("Data from response:", response.data);
        }
      })
      .catch((error) => {
        // handle error
        console.log(error); // I always get an 400 Bad Request
      })
      .finally(() => {
        // always executed
      });

    return props;
  }
...
}

 

 

 

 

Accepted Solution (1)

egillanton
Shopify Partner
11 2 4

This is an accepted solution.

Solution Found:

I had forgotten a crucial step, and that was to update the environment variable  HOST used by the registerWebhooks method.  

The reason I was receiving a 403 Error, was because it was receiving I guess the secret from the previous instance of the running app. Maybe someone can back me up on that. 

 

import { registerWebhook } from "@shopify/koa-shopify-webhooks";

export const registerWebhooks = async (
  shop,
  accessToken,
  type,
  url,
  apiVersion
) => {
  const address = `${process.env.HOST}${url}`;
  const registration = await registerWebhook({
    address: address,
    topic: type,
    accessToken,
    shop,
    apiVersion,
  });

  if (registration.success) {
    console.log(`Successfully registered webhook for ${type}!`);
    console.log(`${address}`);
    console.log("");
  } else {
    console.error("Failed to register webhook", registration.result.data.webhookSubscriptionCreate);
  }
};

 

View solution in original post

Reply 1 (1)

egillanton
Shopify Partner
11 2 4

This is an accepted solution.

Solution Found:

I had forgotten a crucial step, and that was to update the environment variable  HOST used by the registerWebhooks method.  

The reason I was receiving a 403 Error, was because it was receiving I guess the secret from the previous instance of the running app. Maybe someone can back me up on that. 

 

import { registerWebhook } from "@shopify/koa-shopify-webhooks";

export const registerWebhooks = async (
  shop,
  accessToken,
  type,
  url,
  apiVersion
) => {
  const address = `${process.env.HOST}${url}`;
  const registration = await registerWebhook({
    address: address,
    topic: type,
    accessToken,
    shop,
    apiVersion,
  });

  if (registration.success) {
    console.log(`Successfully registered webhook for ${type}!`);
    console.log(`${address}`);
    console.log("");
  } else {
    console.error("Failed to register webhook", registration.result.data.webhookSubscriptionCreate);
  }
};