Full-Stack Troubleshooting: Building a Production Platform from Scratch
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)
destroyPlatform()called beforerenderApplication()— The custom Expressserver.tscalleddestroyPlatform()per-request to "clean up", but this destroyed Angular's DI platform before rendering could complete.withInMemoryScrolling()in shared config —withInMemoryScrolling({ scrollPositionRestoration: 'top' })was in the sharedapp.config.tsused by both browser and SSR. It internally callswindow.scrollTo, which does not exist in Node.js. Fix: move browser-only providers to a separateapp.config.browser.ts.- Missing
APP_BASE_HREF— The viewer was built withbaseHref=/articles/butAPP_BASE_HREFwas not provided in the server config. Angular'sLocationStrategythrows 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:
- 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. - Missing root
@DNS record — OnlywwwA record existed. The root domain@had no A record, so DNS did not resolve. Fix: addA @ 100.55.55.7in Cloudflare DNS. - Wildcard
*record does not cover root — A wildcard A record covers*.example.combut NOTexample.comitself.
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, orlocalStoragedirectly must be guarded withisPlatformBrowser()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.