Full-Stack Troubleshooting: Building a Production Platform from Scratch

Full-Stack Troubleshooting: Building a Production Platform from Scratch
24 views(1 unique)
18 min read
📝
Summary: A week of real-world troubleshooting: Angular 19 SSR NG0401, Google OAuth IPv4 blocking, Cloudflare 521, nginx asset 404s, SSL chain order, Spring Boot mail crash, and logout CORS issues — all diagnosed and fixed.

Introduction

This article documents a week of real-world troubleshooting while building a production platform from scratch — covering Angular 19 SSR, Spring Boot OAuth2, Cloudflare Tunnel, AWS EC2 nginx, SSL certificates, and Google social login. Every issue below was encountered, diagnosed, and solved in a live environment. No lab simulations.

1. Angular SSR: NG0401 NullInjectorError

Symptom

The Angular article viewer (SSR) started and immediately crashed with NG0401: NullInjectorError. The service restarted in a loop every 45 seconds.

Root Causes Found (in order)

  1. destroyPlatform() called before renderApplication() — The custom Express server.ts called destroyPlatform() per-request to "clean up", but this destroyed Angular's DI platform before rendering could complete.
  2. withInMemoryScrolling() in shared config — withInMemoryScrolling({ scrollPositionRestoration: 'top' }) was in the shared app.config.ts used by both browser and SSR. It internally calls window.scrollTo, which does not exist in Node.js. Fix: move browser-only providers to a separate app.config.browser.ts.
  3. Missing APP_BASE_HREF — The viewer was built with baseHref=/articles/ but APP_BASE_HREF was not provided in the server config. Angular's LocationStrategy throws NG0401 when it cannot inject this token during SSR bootstrap.

Fix

// app.config.server.ts
const serverConfig: ApplicationConfig = {
  providers: [
    provideServerRendering(),
    provideServerRoutesConfig(serverRoutes),
    provideNoopAnimations(),
    { provide: APP_BASE_HREF, useValue: '/articles' }  // ← critical
  ]
};

2. Angular SSR: Assets Returning 404

Symptom

The SSR page rendered HTML correctly but all JS/CSS assets returned 404. Browser console showed requests to /main.js, /styles.css — no path prefix.

Root Cause

Angular builds with a relative asset path by default. The SSR app was proxied at /articles in nginx, but assets were requested from the root path / instead of /articles/.

Fix

// angular.json
{
  "options": {
    "baseHref": "/articles/"
  }
}

Also update the Express server to serve static files and SSR routes under the /articles prefix:

app.use('/articles', express.static(browserDistFolder, { index: false }));
app.use('/articles', (req, res) => { /* renderApplication */ });

3. Cloudflare 521: Web Server Down

Symptom

After configuring Cloudflare DNS, all requests to the domain returned Cloudflare error 521 "Web Server Down".

Diagnosis

Cloudflare 521 means the TCP connection to the origin was refused. Three issues were found:

  1. SSL mode "Full" — Cloudflare was connecting to origin on port 443 (HTTPS), but nginx only listened on port 80. Fix: generate a self-signed cert and add listen 443 ssl; to nginx.
  2. Missing root @ DNS record — Only www A record existed. The root domain @ had no A record, so DNS did not resolve. Fix: add A @ 100.55.55.7 in Cloudflare DNS.
  3. Wildcard * record does not cover root — A wildcard A record covers *.example.com but NOT example.com itself.

4. Google OAuth Social Login Hanging

Symptom

Google social login showed the account picker, the user selected an account, but then the page spun indefinitely and eventually Cloudflare returned a 524 timeout.

Diagnosis — 4 separate bugs found

Bug 1: Java IPv4 vs IPv6 routing

The Spring Boot auth server called https://oauth2.googleapis.com/token to exchange the authorization code. curl succeeded in ~1 second by resolving to IPv6 (2607:f8b0:...). But Java's HTTP client resolved the same hostname to an IPv4 address (64.233.185.95) that the ISP silently blocked on outbound port 443.

Evidence: tcpdump showed 9 TCP SYN retransmits to the IPv4 address with zero SYN-ACK replies.

Fix: Pin the IPv6 address in /etc/hosts:

2607:f8b0:4002:c0f::5f oauth2.googleapis.com
2607:f8b0:4002:c0f::5f accounts.google.com
2607:f8b0:4002:c11::5f www.googleapis.com

Bug 2: accessTokenResponseClient not wired

A timeout-aware OAuth2AccessTokenResponseClient bean was defined in AppConfig.java but never wired into the oauth2Login() configuration. Spring Security created its own default client with no timeout at all, causing the Google token exchange to block indefinitely.

.oauth2Login(oauth2 -> oauth2
    .successHandler(oAuth2LoginSuccessHandler)
    .tokenEndpoint(token -> token
        .accessTokenResponseClient(accessTokenResponseClient) // ← was missing
    )
)

Bug 3: Missing message converters on RestTemplate

After wiring the client, login failed with NullPointerException: additionalParameters is null in OidcAuthorizationCodeAuthenticationProvider. The custom RestTemplate was missing OAuth2AccessTokenResponseHttpMessageConverter and FormHttpMessageConverter. Without these, Spring Security 6.3.x cannot deserialize Google's token response correctly.

RestTemplate restTemplate = new RestTemplate(Arrays.asList(
    new FormHttpMessageConverter(),
    new OAuth2AccessTokenResponseHttpMessageConverter()
));

Bug 4: Wrong redirect_uri in PKCE flow

The Angular article viewer was sending redirect_uri=https://example.com/callback (using articleViewerUrl) instead of https://example.com/editor/callback (using authEditorUrl). The auth server rejected it as a mismatch.

// auth.service.ts — wrong
redirect_uri: `${environment.articleViewerUrl}/callback`

// auth.service.ts — correct
redirect_uri: `${environment.authEditorUrl}/callback`

5. Spring Boot Mail Crash on Startup

Symptom

The auth server crashed at startup with: Could not resolve placeholder 'MAIL_USER' in value "${MAIL_USER}". The service restarted in a crash loop.

Root Cause

The application-prod.properties inside the JAR referenced environment variables ${MAIL_USER} and ${MAIL_PASS} with no default values. These variables were not set in the systemd service environment.

Fix Options

Option A (recommended): Add default values to the property placeholders:

spring.mail.username=${MAIL_USER:[email protected]}
spring.mail.password=${MAIL_PASS:your-app-password}

Option B (quick fix): Disable mail entirely while not in use:

spring.autoconfigure.exclude=\
  org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
  org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration

6. Nginx 404 on /articles Assets

Symptom

The SSR viewer page rendered the HTML shell but all JS/CSS assets (/articles/styles.css, /articles/main.js) returned 404. The nginx error log showed: open() "/var/www/editor/articles/styles.css" failed.

Root Cause

nginx had separate location /articles and location ~* \.(js|css)$ blocks. The regex location matched asset requests but served them from the static editor directory, not from the SSR Node.js server.

Fix

Use ^~ prefix modifier on the /articles location to prevent regex locations from intercepting sub-paths:

location ^~ /articles {
    proxy_pass http://127.0.0.1:4202;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_read_timeout 60s;
}

7. Sectigo SSL Certificate Chain Order

Symptom

After installing the Sectigo wildcard certificate, some browsers showed "certificate not trusted" while others worked fine.

Root Cause

The fullchain file was assembled in the wrong order. TLS clients walk the chain from domain cert → intermediate → root. Reverse order causes partial validation failures on strict clients.

Correct chain order

cat STAR_example_com.crt \
    SSL2BUYEMEARSADomainValidationSecureServerCA.crt \
    SectigoPublicServerAuthenticationRootR46_USERTrust.crt \
    > fullchain.crt

Verify with: openssl verify -CAfile ca-bundle.crt STAR_example_com.crt — should return OK. Also confirm the private key matches: both openssl x509 -noout -modulus -in cert.crt | md5sum and openssl rsa -noout -modulus -in private.key | md5sum must produce identical hashes.

8. Logout Not Working After Google Social Login

Symptom

Clicking Sign Out appeared to succeed (token cleared from localStorage) but navigating back to the site immediately showed the user as logged in again.

Root Cause

The Angular logout used fetch() to call the auth server's /logout endpoint. Because the auth server is on a different origin (auth.example.com), the browser's CORS policy blocked the session cookie from being sent cross-origin. The Spring Security session was never invalidated, so the next OAuth2 authorize request auto-authenticated via the live session.

Fix

// Wrong — fetch() cannot send cross-origin cookies
fetch(logoutUrl, { credentials: 'include', redirect: 'manual' })

// Correct — full page navigation sends the cookie properly
window.location.href = logoutUrl;

9. Cloudflare Tunnel "Context Canceled" Errors

Symptom

Cloudflare Tunnel logs showed: ERR error="Incoming request ended abruptly: context canceled" during the Google OAuth callback.

Root Cause

The OAuth token exchange with Google was taking longer than Cloudflare's default tunnel timeout. Cloudflare canceled the request before Spring Boot received the Google token response. This masked the real issue (IPv4 blocking) by making it appear as a tunnel problem.

Resolution

The context canceled errors disappeared entirely after fixing the IPv4/IPv6 DNS issue (see section 4). The token exchange now completes in under 200ms via IPv6, well within all timeout limits.

10. Angular Build Budget Exceeded

Symptom

Production build failed with: src/app/components/article-editor/article-editor.component.scss exceeded maximum budget. Budget 8.00 kB was not met by 543 bytes.

Root Cause

The category selector UI added significant CSS to the article editor component, pushing it past Angular's default anyComponentStyle hard error limit of 8 kB.

Fix

// angular.json — increase component style budget
"budgets": [
  {
    "type": "anyComponentStyle",
    "maximumWarning": "20kB",
    "maximumError": "28kB"
  }
]

Also allowlist Quill's CommonJS dependency to suppress the optimization warning:

"allowedCommonJsDependencies": ["quill", "quill-delta"]

Key Lessons

  • Use tcpdump early — Network-level packet capture revealed the IPv4 blocking issue in minutes. Without it, the symptom (timeout) was identical to a slow server.
  • SSR requires strict browser/server separation — Any API that touches window, document, or localStorage directly must be guarded with isPlatformBrowser() or moved to browser-only config files.
  • Full page navigation for cross-origin auth — Never use fetch() for logout or auth operations that require sending session cookies to a different origin.
  • Cloudflare SSL modes matter — "Full" requires a cert on origin (self-signed OK). "Full (Strict)" requires a CA-signed cert. "Flexible" should only be used during initial setup.
  • Spring Security bean wiring is not automatic — Defining a bean does not mean Spring Security uses it. Explicitly wire custom clients into the security filter chain.

Discussions

No discussions yet. Be the first to start one.

M

Murali Gavarasana

Writer on Ullek

0 articles
0 followers
Writer on the Ullek platform.