Solved

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

egillanton
Shopify Partner
11 2 2

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 2

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 2

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);
  }
};