Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[🐞] Azure/SWA: local preview not working (v3 / v4 conflict in SWA CLI app, workaround included) #7158

Open
rondonjon opened this issue Dec 13, 2024 · 2 comments
Labels
STATUS-1: needs triage New issue which needs to be triaged TYPE: bug Something isn't working

Comments

@rondonjon
Copy link

rondonjon commented Dec 13, 2024

Which component is affected?

Qwik Runtime

Describe the bug

Having installed the azure-swa adapter and successfully built the app, I am unable to preview it locally using npx swa start.

> npx swa start

Welcome to Azure Static Web Apps CLI (1.1.10)

Using configuration "qwik-app" from file:
  .....\qwik-swa-repro\swa-cli.config.json

***********************************************************************
* WARNING: This emulator may not match the cloud environment exactly. *
* Always deploy and test your app in Azure.                           *
***********************************************************************

[api] Can't determine project language from files. Please use one of [--dotnet-isolated, --dotnet, --javascript, --typescript, --java, --python, --powershell, --custom]
[api] Can't determine project language from files. Please use one of [--dotnet-isolated, --dotnet, --javascript, --typescript, --java, --python, --powershell, --custom]
[api] Can't determine project language from files. Please use one of [--dotnet-isolated, --dotnet, --javascript, --typescript, --java, --python, --powershell, --custom]
[api]
[api] Azure Functions Core Tools
[api] Core Tools Version:       4.0.6280 Commit hash: N/A +421f0144b42047aa289ce691dc6db4fc8b6143e6 (32-bit)
[api] Function Runtime Version: 4.834.3.22875
[api]
[api] Can't determine project language from files. Please use one of [--dotnet-isolated, --dotnet, --javascript, --typescript, --java, --python, --powershell, --custom]
[api] Can't determine project language from files. Please use one of [--dotnet-isolated, --dotnet, --javascript, --typescript, --java, --python, --powershell, --custom]
[api] Can't determine project language from files. Please use one of [--dotnet-isolated, --dotnet, --javascript, --typescript, --java, --python, --powershell, --custom]
[api] [2024-12-13T16:11:30.848Z] The 'FUNCTIONS_WORKER_RUNTIME' setting is required. Please specify a valid value. See https://go.microsoft.com/fwlink/?linkid=2257963 for more information. The application will continue to run, but may throw an exception in a future release.
[api] [2024-12-13T16:11:30.896Z] No job functions found. Try making your job classes and methods public. If you're using binding extensions (e.g. Azure Storage, ServiceBus, Timers, etc.) make sure you've called the registration method for the extension(s) in your startup code (e.g. builder.AddAzureStorage(), builder.AddServiceBus(), builder.AddTimers(), etc.).
[api] For detailed output, run func with --verbose flag.
[swa]
[swa] Found configuration file:
[swa]   .....\qwik-swa-repro\public\staticwebapp.config.json
[swa]
[swa]
[swa] Serving static content:
[swa]   .....\qwik-swa-repro\dist
[swa]
[swa] Serving API:
[swa]   .....\qwik-swa-repro\azure-functions
[swa]
[swa] Azure Static Web Apps emulator started at http://localhost:4280. Press CTRL+C to exit.
[swa]
[swa]
[api] [2024-12-13T16:11:35.849Z] Host lock lease acquired by instance ID '000000000000000000000000464D5A11'.
[swa] GET http://127.0.0.1:7071/api/render (proxy)
[swa] GET http://localhost:4280/api/render - 404

The server is running, but with mixed results:

  • for / and for /index.html it responds a 404
  • some static files like robots.txt or manifest.json are correctly delivered
  • overall the app isn't working

Reproduction

System Info

System:
    OS: Windows 11 10.0.22631
    CPU: (32) x64 AMD Ryzen 9 7945HX with Radeon Graphics
    Memory: 15.17 GB / 31.19 GB
  Binaries:
    Node: 20.18.1 - C:\Program Files\nodejs\node.EXE
    npm: 10.8.2 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Edge: Chromium (127.0.2651.74)
    Internet Explorer: 11.0.22621.3527
  npmPackages:
    @builder.io/qwik: ^1.11.0 => 1.11.0
    @builder.io/qwik-city: ^1.11.0 => 1.11.0
    typescript: 5.4.5 => 5.4.5
    undici: * => 7.1.0
    vite: 5.3.5 => 5.3.5

Additional Information

No response

@rondonjon rondonjon added STATUS-1: needs triage New issue which needs to be triaged TYPE: bug Something isn't working labels Dec 13, 2024
@rondonjon rondonjon reopened this Dec 13, 2024
@rondonjon
Copy link
Author

OK, after several days of debugging and searches, I think I found the root cause. I am leaving this here as an update, hoping that someone else may find it useful in the future.

TL;DR: ignore the errors about v4 in swa start, don't use local previews at all, downgrade to v3 of the Azure Core Function Tools if you have v4 installed, and try a deployment to Azure. If this works for you, use the env flag of the SWA CLI app to deploy to preview environments to test the app before targeting production.

Recap: the Qwik adapter relies on the SWA CLI app, which deploys Static Web Applications on Azure, which, despite the name, aren't necessarily fully static, but may have an API with cloud functions. The Qwik adapter executes an additional Vite build to produce a bundle in /azure-functions which contains a server function which is then mounted to /api and targeted by rewriting requests to pages and endpoints on the server-side.

I was getting the error above because SWA downloads the latest function tools (currently v4) for swa start. This causes some errors, mainly because the Qwik adapter generates code for the v3 functions API, but also due to a bug that caused no suitable download to be found for Windows users for several months.

I spent some time to migrate the Qwik adapter to v4 locally and was eventually able to get the local previews to work. At the same time, however, I realized that the deployments to Azure were no longer working.

As it turns out, v4 of the functions API has been "GA" for over a year now, and most Azure products (including the Functions App) do in fact use v4 as the default by now, and while SWA only works with v4 locally, it still relies on v3 during deployments. The implementation of v4 support for deployments is still pending.

@rondonjon
Copy link
Author

Should a developer want to migrate the Azure SWA adapter to v4, the following may be of help.

--

official v3 -> v4 migration guide

--

function.json is no longer needed. The function handlers should now be exposed programmatically in the entry file (see below).

--

package.json must reference the entry point in its "main" field, like so:

{
   "type": "module",
   "main": "render/index.mjs"
}

--

host.json is no longer needed and can be removed

--

Middleware has to use new types, in particular for context and request.

The order of context and request has changed.

Headers are now supplied in a different structure.

import type {
  FunctionHandler,
  InvocationContext,
  HttpRequest,
} from "@azure/functions";
import { setServerPlatform } from "@builder.io/qwik/server";
import { requestHandler } from "@builder.io/qwik-city/middleware/request-handler";
import type {
  ServerRenderOptions,
  ServerRequestEvent,
} from "@builder.io/qwik-city/middleware/request-handler";
import { getNotFound } from "@qwik-city-not-found-paths";
import {
  _deserializeData,
  _serializeData,
  _verifySerializable,
} from "@builder.io/qwik";
import { parseString } from "set-cookie-parser";
import { isStaticPath } from "@qwik-city-static-paths";

interface AzureResponse {
  status: number;
  headers: { [key: string]: unknown };
  body?: string | Uint8Array;
  cookies?: AzureCookie[];
}

interface AzureCookie {
  /** Cookie name */
  name: string;
  /** Cookie value */
  value: string;
  /** Specifies allowed hosts to receive the cookie */
  domain?: string;
  /** Specifies URL path that must exist in the requested URL */
  path?: string;
  /**
   * NOTE: It is generally recommended that you use maxAge over expires. Sets the cookie to expire
   * at a specific date instead of when the client closes. This can be a Javascript Date or Unix
   * time in milliseconds.
   */
  expires?: Date | number;
  /** Sets the cookie to only be sent with an encrypted request */
  secure?: boolean;
  /** Sets the cookie to be inaccessible to JavaScript's Document.cookie API */
  httpOnly?: boolean;
  /** Can restrict the cookie to not be sent with cross-site requests */
  sameSite?: string | undefined;
  /**
   * Number of seconds until the cookie expires. A zero or negative number will expire the cookie
   * immediately.
   */
  maxAge?: number;
}

/** @public */
export function createQwikCity(opts: QwikCityAzureOptions): FunctionHandler {
  const qwikSerializer = {
    _deserializeData,
    _serializeData,
    _verifySerializable,
  };

  if (opts.manifest) {
    setServerPlatform(opts.manifest);
  }

  const onAzureSwaRequest: FunctionHandler = async (
    req: HttpRequest,
    context: InvocationContext
  ): Promise<AzureResponse> => {
    try {
      const headers = Array.from(req.headers.entries()).reduce(
        (acc, [key, value]) => {
          acc[key] = value;
          return acc;
        },
        {} as Record<string, string>
      );

      const url = new URL(headers["x-ms-original-url"]);

      const options: RequestInit = {
        method: req.method || "GET",
        headers,
        body: /*req.bufferBody || req.rawBody ||*/ req.body,
      };

      const serverRequestEv: ServerRequestEvent<AzureResponse> = {
        mode: "server",
        locale: undefined,
        url,
        platform: context,
        env: {
          get(key) {
            return process.env[key];
          },
        },
        request: new Request(url, options),
        getWritableStream: (status, headers, cookies, resolve) => {
          const response: AzureResponse = {
            status,
            body: new Uint8Array(),
            headers: {},
            cookies: cookies.headers().map((header) => parseString(header)),
          };
          headers.forEach((value, key) => {
            response.headers[key] = value;
          });
          return new WritableStream({
            write(chunk: Uint8Array) {
              if (response.body instanceof Uint8Array) {
                const newBuffer = new Uint8Array(
                  response.body.length + chunk.length
                );
                newBuffer.set(response.body);
                newBuffer.set(chunk, response.body.length);
                response.body = newBuffer;
              }
            },
            close() {
              resolve(response);
            },
          });
        },

        getClientConn: () => {
          return {
            ip: headers["x-forwarded-client-Ip"],
            country: undefined,
          };
        },
      };

      // send request to qwik city request handler
      const handledResponse = await requestHandler(
        serverRequestEv,
        opts,
        qwikSerializer
      );

      if (handledResponse) {
        handledResponse.completion.then((err) => {
          if (err) {
            console.error(err);
          }
        });

        const response = await handledResponse.response;

        if (response) {
          return response;
        }
      }

      // qwik city did not have a route for this request
      // response with 404 for this pathname

      // In the development server, we replace the getNotFound function
      // For static paths, we assign a static "Not Found" message.
      // This ensures consistency between development and production environments for specific URLs.
      const notFoundHtml = isStaticPath(req.method || "GET", url)
        ? "Not Found"
        : getNotFound(url.pathname);

      return {
        status: 404,
        headers: {
          "Content-Type": "text/html; charset=utf-8",
          "X-Not-Found": url.pathname,
        },
        body: notFoundHtml,
      };
    } catch (e: unknown) {
      console.error(e);
      return {
        status: 500,
        headers: {
          "Content-Type": "text/plain; charset=utf-8",
        },
      };
    }
  };

  return onAzureSwaRequest;
}

/** @public */
export interface QwikCityAzureOptions extends ServerRenderOptions {}

/** @public */
export interface PlatformAzure extends Partial<InvocationContext> {}

--

A new index.mjs entry file registers the handler on app instead of exporting it:

/*
 * WHAT IS THIS FILE?
 *
 * It's the entry point for the Azure Static Web Apps middleware when building for production.
 *
 * Learn more about the Azure Static Web Apps integration here:
 * - https://qwik.dev/docs/deployments/azure-swa/
 *
 */
import { createQwikCity, type PlatformAzure } from "./azure-v4-middleware";
import { app } from "@azure/functions";
import qwikCityPlan from "@qwik-city-plan";
import { manifest } from "@qwik-client-manifest";
import render from "./entry.ssr";

declare global {
	interface QwikCityPlatform extends PlatformAzure {}
}

const handler = createQwikCity({
	render,
	qwikCityPlan,
	manifest,
});

app.http("render", {
	methods: [
		"CONNECT",
		"DELETE",
		"GET",
		"HEAD",
		"OPTIONS",
		"PATCH",
		"POST",
		"PUT",
		"TRACE",
	],
	authLevel: "anonymous",
	handler,
});

@rondonjon rondonjon changed the title [🐞] Azure/SWA: local preview not working [🐞] Azure/SWA: local preview not working (v3 / v4 conflict in SWA CLI app, workaround included) Dec 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
STATUS-1: needs triage New issue which needs to be triaged TYPE: bug Something isn't working
Projects
None yet
Development

No branches or pull requests

1 participant