Handling POST requests in AEM with custom Sling Servlets

When AEM developers first start wiring up contact forms, search filters, or content ingestion endpoints, the Sling servlet framework often feels like a maze of annotations and resource types. The good news is that handling POST requests for form submissions follows a fairly predictable pattern once you understand how Sling resolves servlets and how request parameters flow through the framework. Engineers in Sydney and Melbourne who have been maintaining AEM sites for major Australian brands like ANZ, Telstra, and News Corp Australia will tell you that nearly every integration ends up needing a POST handler somewhere along the line.

Sling's URL decomposition model means a POST can land on your code because of a resource type, a path, an extension, or a selector. Choosing the right binding strategy up front is the difference between a servlet that runs reliably across author and publish instances and one that mysteriously stops firing after a dispatcher cache flush. The patterns below have been refined through countless AEM User Group meetups from Brisbane to Perth and tend to be the ones that survive contact with production traffic.

Why Sling resolves POST handlers differently

Sling is not a traditional servlet container. Instead of mapping URLs to servlets through web.xml, Sling decomposes the request URL into a resource, then picks a servlet based on the resource's type and the request's selectors and extensions. For POST submissions coming from an HTML form, the URL usually points at a component resource or a dedicated path under /content, and the form's method="post" ensures the request reaches your doPost method. This indirection is what allows AEM authors to drop a component onto a page and immediately get a working endpoint without redeploying code, but it also means a misconfigured sling:resourceType can silently route the POST to the wrong servlet.

In practice, Australian AEM teams tend to prefer path-based bindings for public-facing forms because they are easier to debug in the Felix console and play nicely with Dispatcher caching rules. Resource type bindings shine when you want the same form component to behave identically wherever it appears on a site, including on translated pages served from language masters. Whichever approach you pick, the underlying contract is the same: extend SlingAllMethodsServlet, override doPost(SlingHttpServletRequest, SlingHttpServletResponse), and let Sling handle the routing. Avoiding SlingSafeMethodsServlet for form submissions is critical because that base class deliberately rejects any non-safe HTTP verb.

Building the servlet with the right annotations

Once you have decided between path-based and resource type binding, the servlet class itself is refreshingly small. A typical implementation starts with @Component to mark the OSGi service, @Service to expose it to the SCR, and one of the binding annotations: @SlingServletPaths for an explicit path or @SlingServletResourceTypes for resource type binding. Adding methods = {"POST"} in the appropriate annotation array ensures the servlet only reacts to form submissions and never accidentally exposes a GET endpoint that could leak data through browser history or referrer logs.

Service ranking matters more than many developers realise. Two servlets that match the same resource type will cause Sling to pick the higher-ranked service, and the loser silently disappears. Setting serviceRanking to a positive integer, somewhere between 100 and 1000 depending on how specific your binding is, removes ambiguity and makes future debugging easier. Australian teams that have moved to AEM as a Cloud Service often standardise on rankings above 500 for custom POST handlers so they override any out-of-the-box behaviour shipped by Adobe. Pinning the servlet to a specific resource super-type, such as wcm/foundation/components/page, is another common pattern when the form lives on a template that designers might extend.

Reading and validating form payload

Inside doPost, the first job is to pull the submitted values out of the request. The SlingHttpServletRequest interface gives you getRequestParameter(String name) for single values and getRequestParameterList(String name) for repeated fields like checkbox groups or multi-select dropdowns. For file attachments posted through multipart/form-data, getFiles() returns an iterable of RequestParameter objects, each of which exposes both an input stream and the original filename. Wrapping that stream in a try-with-resources block keeps the heap clean and prevents the leaked file handle warnings that occasionally show up in AEM's error.log after heavy form traffic.

Validation is where most homegrown handlers fall over. Skipping it entirely is tempting because AEM already runs inside a hardened container, but production forms in Australia routinely receive submissions from overseas bot networks probing for XSS or SQL injection. A small validator utility that trims strings, enforces maximum lengths, and rejects parameters whose values do not match expected patterns will save the support team from paging you at 2am. Developers working on government or financial services projects, which is a sizeable slice of the AEM work in Canberra and Sydney, often layer in JSON Schema or a lightweight rules engine so business stakeholders can tweak validation rules without a redeploy.

Persisting submissions and shaping the response

Once the payload is clean, you need somewhere to send it. The two paths most teams pick are writing to the JCR repository under a dedicated /content/forms/submissions tree or forwarding to an external system through a queue or HTTP client. For low-volume contact forms, dropping a nt:unstructured node straight into the repository is fine and gives authors a familiar touch UI to review entries. For anything that feeds into a downstream system, a session-bound service that posts to Apache NiFi or a Kafka topic is the more durable pattern, and there is useful background on that approach in a recent piece on AEM and Apache NiFi pipelines.

Shaping the response is often forgotten until the front-end team complains. A POST handler that returns nothing leaves the browser hanging, and one that returns a full HTML page breaks the SPA-style interactions that have become standard across Australian retail and media sites. The common compromise is to send a 302 redirect to a thank-you page or to respond with a small JSON payload and an appropriate Content-Type header so the front end can update the DOM in place. Either way, the status code matters: returning 200 for a server-side error masks problems from monitoring, while using 400 or 422 tells the browser, and your APM tool, that the submission actually failed.

Hardening the endpoint for production

A form endpoint that lives on publish is a public endpoint, and that means treating it with the same suspicion as any internet-facing API. The Dispatcher should be configured to never cache POST responses, and CSRF tokens should be either generated by the AEM framework or hand-rolled and stored in the user session. Rate limiting at the Dispatcher or load balancer level is a worthwhile safeguard for endpoints that handle high volumes, particularly those exposed to campaigns running across AEST business hours when legitimate traffic peaks.

Logging is the final piece, and it is where many teams skimp. Writing the submission payload, the resolved user, the resource path, and the outcome to a structured log entry makes post-incident analysis far less painful. If the endpoint forwards to an external service, log the correlation ID returned by that service so you can trace a single submission from the browser all the way through to the warehouse. For teams running AEM in the cloud, forwarding those logs to a centralised platform such as Splunk or Datadog keeps on-call rotations sane, and it means an incident in Brisbane at 3am does not require someone to SSH into the publish instance to figure out what happened. Engineers who want to dig deeper into these patterns alongside other AEM practitioners can find plenty of detailed sessions on the conference website, which archives talks from previous years covering servlet design, Dispatcher tuning, and Cloud Service migrations.

If you want to see these patterns demonstrated live and trade war stories with other AEM engineers, seats for the workshops fill up quickly once the event registration page opens, and the agenda lists which sessions extend these servlet patterns into reactive microservices and edge-rendered forms. Several of the accepted talks focus on event-driven architectures that complement the synchronous POST patterns outlined above, which should make the trip worthwhile for anyone maintaining AEM stacks across the Asia-Pacific region.