> For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt.

Before we start — the one question that decides this topic
Your syllabus point 15 says servlets are registered two ways and asks what the difference is. Almost every candidate answers that with something like:

"By path you give it a URL, by resource type you tie it to a component."

That is true, and it scores about half a mark. It describes the syntax but misses the two things that actually matter:

Path-bound servlets bypass content permissions. There is no resource, so there is nothing for Sling to check ACLs against. You have to do authorisation yourself, and most people don't.
Path-bound servlets have to be whitelisted in the dispatcher. Resource-type servlets sit on real content paths, which are already allowed.
Those two are why Adobe recommends resource-type registration and treats path binding as the exception. If you lead with security and dispatcher behaviour rather than syntax, you have given a clearly better answer than the room.

There is also a strong continuity thread here. File 02's flagship component story — the tabbed listing with Load More — hinged on registering a servlet by resource type with a selector and suffix so the responses stayed dispatcher-cacheable. That decision lives in this file. If you tell that story, expect to be asked to justify it here.

1. Introduction
   1.1 What is a servlet, and why do you need one in AEM?
   A servlet is a Java class that handles an HTTP request and writes a response. That is standard Java EE, not AEM-specific.

But here is the question worth asking first, because interviewers do: AEM already renders pages through HTL and Sling Models. So when do you actually need a servlet?

You don't need one for rendering a page. That is what components and HTL are for.

You need one when the browser needs data rather than a page. Three real cases from our project:

Case one — the browser asks for more data after the page has loaded. The product listing's Load More button. The page is already rendered; JavaScript needs the next batch of cards. That is not a page render, so HTL is the wrong tool.

Case two — a form submits somewhere that isn't a repository node. A "request a quote" form that has to reach an external CRM. The Sling Post Servlet writes to the repository, which is not what we want here.

Case three — an alternative representation of existing content. The same product page as JSON, for the mobile app or a partner integration.

The interview answer:

"In AEM most rendering goes through HTL and Sling Models, so I write a servlet when the browser needs data rather than a page. Typically that's three cases: an AJAX call after page load, like a Load More or a filter; a form posting somewhere that isn't a repository node; or exposing existing content in another format, usually JSON for a front-end or a partner system.

If I find myself writing a servlet to render HTML for a page, that's usually a sign I should have used a component instead."

1.2 A servlet is just an OSGi component
This is the bridge from file 06, and it explains everything about how servlets are registered.

A Sling servlet is an OSGi component registered as a service under the javax.servlet.Servlet interface.

@Component(service = Servlet.class)      // ← this is the whole mechanism
public class ProductSpecsServlet extends SlingSafeMethodsServlet \{
}Copy
Copy
That is it. Registering under Servlet.class puts it on the noticeboard from file 06. Sling's servlet resolver watches that noticeboard and reads each servlet's service properties to decide which requests it should handle.

Two consequences worth stating in an interview, because they tie the files together:

One — @Reference works in a servlet. File 05 explained why it does not work in a Sling Model: Declarative Services only manages @Component classes. A servlet is one, so DS manages it and can inject into it.

Two — a servlet can be unsatisfied. All of file 06's component lifecycle applies. If a servlet has a mandatory @Reference to a service that isn't registered, the servlet component never activates — and you get no error, just a URL that returns 404 forever. That is a genuinely common and confusing production problem.

1.3 A real project example to adapt
"We have around eight servlets. Most are registered by resource type — the Load More endpoint for our product listing, a specifications endpoint the product page calls, and a couple of JSON endpoints for the mobile app. Two are path-bound: a form handler that posts to an external CRM, and a health check endpoint. For those two we do our own authorisation, because path-bound servlets bypass content ACLs, and both paths are explicitly whitelisted in the dispatcher configuration."

That covers the split, the security consequence, and the dispatcher — three follow-ups pre-empted.

2. Core Concepts
   2.1 The two base classes (syllabus point 16)
   Your syllabus asks when you extend each. Let's start here because it shapes the code you write.

There are two abstract classes, and one extends the other:

javax.servlet.Servlet                        (interface)
↑
GenericServlet
↑
SlingSafeMethodsServlet        ← GET, HEAD, OPTIONS, TRACE
↑
SlingAllMethodsServlet         ← adds POST, PUT, DELETECopy
Copy
A small precision point, since your syllabus says "extend/implement": you extend these — they are abstract classes, not interfaces. You would implement javax.servlet.Servlet, but in practice nobody does that directly. Saying "extend" correctly is a small but real signal.

SlingSafeMethodsServlet handles only the HTTP methods that are safe — meaning they read but do not change anything:

Method	What it does
GET	Retrieve something
HEAD	Like GET but headers only
OPTIONS	What methods does this support?
TRACE	Diagnostic echo
You override doGet, and occasionally doHead.

SlingAllMethodsServlet extends the safe one and adds the unsafe methods — the ones that change state:

Method	What it does
POST	Create, or submit
PUT	Replace
DELETE	Remove
You override doPost, doPut, doDelete, plus anything from the safe set.

Now the question your syllabus is actually asking: when do you use each?

The rule is one sentence:

If the servlet only reads, extend SlingSafeMethodsServlet. If it changes anything, extend SlingAllMethodsServlet.

And the follow-up that separates a good answer: why not just always use SlingAllMethodsServlet?

Three reasons:

One — least privilege. Extending SlingAllMethodsServlet means your servlet advertises that it handles POST, PUT and DELETE. If you only implemented doGet, you have declared an interface you did not build. The inherited methods return 405 Method Not Allowed, so nothing breaks — but you have widened the surface for no reason.

Two — intent. A reader seeing SlingSafeMethodsServlet knows immediately this endpoint is read-only. That is free documentation.

Three — code review. Extending SlingAllMethodsServlet for a read-only servlet is a standard code review comment, and on a security-reviewed project it will be raised.

The interview answer:

"SlingSafeMethodsServlet handles the safe HTTP methods — GET, HEAD, OPTIONS and TRACE — meaning the ones that read without changing state. SlingAllMethodsServlet extends it and adds POST, PUT and DELETE.

So the rule is: read-only servlet extends the safe one, anything that writes extends the all-methods one.

I wouldn't just always use SlingAllMethodsServlet, even though it would work, for three reasons. It advertises methods I haven't implemented — they'd return 405, so nothing breaks, but it's a wider surface than needed. It loses the intent, because SlingSafeMethodsServlet tells the next developer at a glance that this endpoint is read-only. And it's a routine code review finding on any security-conscious project.

Small precision: these are abstract classes, so you extend them. You'd only implement javax.servlet.Servlet directly, which nobody does in practice."

2.2 Registering by path (syllabus point 15)
@Component(service = Servlet.class)
@SlingServletPaths("/bin/energy/quote-request")
public class QuoteRequestServlet extends SlingAllMethodsServlet \{

```
@Override
protected void doPost(SlingHttpServletRequest request,
                      SlingHttpServletResponse response) { }
```

}Copy
Copy
What this means: the servlet answers at that exact URL. [https://www.example.com/bin/energy/quote-request](https://www.example.com/bin/energy/quote-request).

The URL is arbitrary. It does not correspond to anything in the repository. There is no node at /bin/energy/quote-request.

And that last sentence is the source of everything important about path binding, which we come to in 2.4.

The older property-based form, which you will see in existing codebases:

@Component(
service = Servlet.class,
property = \{
"sling.servlet.paths=/bin/energy/quote-request",
"sling.servlet.methods=POST"
})Copy
Copy
Same thing, less readable. The annotation is current.

A constraint worth knowing: recent AEM and Sling versions restrict which path prefixes a path-bound servlet may use, and there is an allowlist configuration in the Sling Servlet Resolver. Conventionally these live under /bin/. Registering a servlet at an arbitrary top-level path is either discouraged or blocked depending on your version, so /bin/<project>/<name> is the pattern to follow.

2.3 Registering by resource type (syllabus point 15)
@Component(service = Servlet.class)
@SlingServletResourceTypes(
resourceTypes = "energy/components/categorylisting",
selectors = "cards",
extensions = "html",
methods = HttpConstants.METHOD\_GET
)
public class CategoryCardsServlet extends SlingSafeMethodsServlet \{
}Copy
Copy
What this means: this servlet handles requests for any content node whose sling:resourceType is energy/components/categorylisting, when the request has the selector cards, the extension html, and the method GET.

So it answers at URLs like:

/content/energy/global/en/products.cards.html/2
└─── a real content path ───┘  └sel┘└ext┘└suffix┘Copy
Copy
Notice this is exactly the Sling resolution model from file 01 — URL → Resource → Resource Type → Script. A resource-type servlet slots into that chain in place of an HTL script. It is not a separate mechanism; it is the same one.

The four attributes:

Attribute	Purpose	Required?
resourceTypes	Which content this serves	Yes
selectors	Narrow to a selector, e.g. cards	No, but almost always used
extensions	html, json, csv	Recommended
methods	GET, POST, …	Recommended
Why you almost always want a selector. Without one, your servlet takes over every GET for that resource type — including the page render itself. With selectors = "cards", the normal page still renders through HTL and only .cards. requests reach your servlet.

The older property form:

@Component(
service = Servlet.class,
property = \{
"sling.servlet.resourceTypes=energy/components/categorylisting",
"sling.servlet.selectors=cards",
"sling.servlet.extensions=html",
"sling.servlet.methods=GET"
})Copy
Copy
2.4 The real difference — and why it matters (syllabus point 15)
This is the heart of the file. Four differences, and the first two are the ones that count.

Difference 1 — permissions
This is the most important one, and most candidates miss it.

With a resource-type servlet, the URL is a real content path. So Sling does what it always does: it resolves the path to a resource using the current user's permissions. If an anonymous visitor cannot read that content, the resource does not resolve and they get a 404 before your code runs at all.

You get content permissions for free.

With a path-bound servlet, there is no resource. /bin/energy/quote-request is not a node. So there is nothing for Sling to check permissions against, and your servlet runs for anyone who knows the URL.

You must do authorisation yourself, in code. And if you forget — which is common — you have an unauthenticated endpoint on your public site.

"The difference people usually miss is permissions. A resource-type servlet sits on a real content path, so Sling resolves that resource using the current user's session and content ACLs apply automatically — if the user can't read the content, they never reach my code. A path-bound servlet has no resource behind it, so there's nothing to check against and it runs for anyone who knows the URL. That means every path-bound servlet has to do its own authorisation, and forgetting is how you end up with an unauthenticated endpoint on a public site."

Difference 2 — the dispatcher
From file 01: the dispatcher denies everything by default and whitelists what the site needs.

A resource-type servlet lives under /content, which is already allowed. Nothing extra to do.

A path-bound servlet lives under /bin, which is not allowed by default. It must be explicitly whitelisted:

/0200 \{ /type "allow" /path "/bin/energy/\*" /method "POST" }Copy
Copy
And this produces a very common bug: the servlet works perfectly on author and locally, and returns 404 on the published site — because the dispatcher rejected the request before it ever reached AEM. It is the same shape of failure as allowProxy in file 04: environment-specific, and the code is fine.

Difference 3 — caching
A resource-type servlet with a selector and extension produces a URL that is part of the path, so the dispatcher can cache it as a file.

/content/energy/global/en/products.cards.html/2      ← cacheable
/bin/energy/cards?page=2                              ← not cacheableCopy
Copy
This is exactly the decision in file 02's Load More story. Same data, but one form caches and the other hits publish on every click.

Difference 4 — flexibility
Path binding gives you any URL you like, with no content required. Resource-type binding requires real content to exist at that path.

That is the genuine advantage of path binding, and it is why it still exists.

The comparison table
By Path	By Resource Type
Annotation	@SlingServletPaths	@SlingServletResourceTypes
URL	Arbitrary, e.g. /bin/energy/x	A real content path with a selector
Backed by a real resource	No	Yes
Content ACLs apply	No — do it yourself	Yes, automatically
Dispatcher	Must be whitelisted	Already allowed
Cacheable	Rarely	Yes, with selector + extension
Needs content to exist	No	Yes
Adobe's recommendation	Use sparingly	Preferred
Good for	Global utilities, external form handlers, health checks	Almost everything else
When to actually use path binding
Be specific here, because "never" is a weak answer:

Use a path-bound servlet when there is genuinely no content context. Three legitimate cases:

A form handler posting to an external system, where no repository node is involved.
A health check or diagnostic endpoint.
A webhook receiver where an external system must call a fixed, stable URL.
Everything else should be resource-type bound.

The complete interview answer:

"Two mechanisms. @SlingServletPaths binds to a fixed URL like /bin/energy/export. @SlingServletResourceTypes binds to a resource type plus selector, extension and method, so it answers on real content paths.

The syntax difference is the obvious part. The two that actually matter are security and the dispatcher.

Security: a resource-type servlet resolves a real resource, so content ACLs are applied automatically — an anonymous user who can't read that content gets a 404 before my code runs. A path-bound servlet has no resource, so nothing is checked and it runs for anyone with the URL. Authorisation becomes my job.

Dispatcher: /content paths are already allowed, but /bin has to be explicitly whitelisted. That's why a path-bound servlet frequently works on author and 404s on the live site.

There's also caching. A resource-type servlet with a selector produces a path-based URL the dispatcher can cache as a file. A path-bound one usually takes query parameters, which aren't cacheable — that difference was significant on a listing component we built with a Load More.

So I default to resource type. I use path binding only when there's genuinely no content context — a form handler posting to an external CRM, a health check, or a webhook that needs a fixed URL. And when I do, I write explicit authorisation and add the dispatcher rule."

2.5 The methods — GET, POST, PUT, DELETE (syllabus point 15)
Your syllabus says to memorise examples of each. Note the signature: SlingHttpServletRequest, not the plain HttpServletRequest. The Sling versions give you getResource(), getResourceResolver() and getRequestPathInfo().

@Override
protected void doGet(SlingHttpServletRequest request,
SlingHttpServletResponse response)
throws ServletException, IOException \{ }

@Override
protected void doPost(SlingHttpServletRequest request,
SlingHttpServletResponse response)
throws ServletException, IOException \{ }

@Override
protected void doPut(SlingHttpServletRequest request,
SlingHttpServletResponse response)
throws ServletException, IOException \{ }

@Override
protected void doDelete(SlingHttpServletRequest request,
SlingHttpServletResponse response)
throws ServletException, IOException \{ }Copy
Copy
Four things they all share:

protected, not public.
Return void — you write to the response, you do not return a value.
Throw ServletException and IOException.
Take the Sling request and response types.
What each is for, semantically — worth being able to say, because it shows you understand HTTP rather than just AEM:

Method	Meaning	Safe?	Idempotent?
GET	Read something	Yes	Yes
POST	Create, or submit	No	No
PUT	Replace at a known location	No	Yes
DELETE	Remove	No	Yes
"Idempotent" means calling it twice has the same effect as calling it once. PUT and DELETE are — deleting the same thing twice leaves it deleted. POST is not — posting a form twice creates two records. That is why browsers warn you before re-submitting a form, and it is a good detail to drop in.

Full working examples of all four are in section 8.

2.6 Every servlet and service annotation (syllabus point 14)
Your syllabus asks for all the annotations for services and servlets together. Services were file 06; here is the combined map.

Servlet registration (org.apache.sling.servlets.annotations):

Annotation	Purpose
@SlingServletResourceTypes	Register by resource type — preferred
@SlingServletPaths	Register by fixed path
@SlingServletName	Give it a readable name in the consoles
@SlingServletFilter	Register a request filter
Always paired with:

@Component(service = Servlet.class)Copy
Copy
@SlingServletResourceTypes attributes:

@SlingServletResourceTypes(
resourceTypes = "energy/components/categorylisting",   // required
resourceSuperType = "...",                             // optional
selectors = \{ "cards", "list" },                       // optional, use one
extensions = \{ "html", "json" },
methods = \{ HttpConstants.METHOD\_GET }
)Copy
Copy
@SlingServletPaths:

@SlingServletPaths(\{ "/bin/energy/quote", "/bin/energy/quote/v2" })Copy
Copy
Note it takes no methods attribute — you control that by which doXxx methods you override, and by your base class.

The old property names, so you can read legacy code:

sling.servlet.resourceTypes
sling.servlet.resourceSuperType
sling.servlet.selectors
sling.servlet.extensions
sling.servlet.methods
sling.servlet.paths
sling.servlet.prefix          ← search-path priority: "/apps" beats "/libs"Copy
Copy
Service annotations from file 06, which servlets also use:

Annotation	Purpose in a servlet
@Component(service = Servlet.class)	Registers it
@Reference	Inject a service — works because a servlet is an OSGi component
@Activate / @Deactivate	Lifecycle, if the servlet holds resources
@ObjectClassDefinition / @Designate	Configure the servlet per run mode
@ServiceDescription	A human-readable description in the console
The one-line summary for point 14:

"For servlets it's @Component(service = Servlet.class) plus either @SlingServletResourceTypes or @SlingServletPaths, and @SlingServletFilter for filters. Because a servlet is an OSGi component, everything from the service side applies too — @Reference for injection, @Activate and @Deactivate for lifecycle, and @ObjectClassDefinition with @Designate if it needs configuration. The older style used sling.servlet.\* properties on @Component directly, which still works but is much less readable."

2.7 The default servlets — the ones you didn't write
Two servlets ship with Sling and handle requests you never wrote code for. Both come up in interviews, and both have security implications.

DefaultGetServlet handles a GET when nothing else matches. It renders a node as .json, .html, .xml or .res.

This is what serves .infinity.json — and it is why /content.infinity.json can dump your repository if the dispatcher does not block it. That connects straight back to file 01.

It is configurable — you can disable the JSON renderer, and cap how many nodes .infinity.json will return.

SlingPostServlet handles a POST to a repository path, creating and updating nodes from form fields.

This is what saves your component dialogs — the mechanism from file 02. Nobody writes a servlet for a dialog because this one already exists.

Its special parameters are worth knowing:

Parameter	Effect
:operation	delete, move, copy, import
@TypeHint	Force a property type, e.g. Boolean, Long, Date
@Delete	Remove a property
@ValueFrom	Take the value from a differently-named field
:redirect	Where to go after the POST
:status	browser for a redirect, or return a plain status
Why this matters for security: the Sling Post Servlet means any authenticated user who can write to a path can create content there by POSTing to it. On publish that is why POST is normally blocked at the dispatcher for content paths.

3. Internal Working
   3.1 How Sling picks a servlet
   This extends the script resolution from file 01, and it explains why path-bound servlets behave differently.

graph TD
A\["Request arrives"] --> B\{"Does a PATH-BOUND<br/>servlet match the URL?"}
B -->|Yes| C\["Use it.<br/>No resource resolved.<br/>No ACL check."]
B -->|No| D\["Resolve the URL to a RESOURCE<br/>— user permissions apply here"]
D --> E\{"Resource exists<br/>and is readable?"}
E -->|No| F\["404"]
E -->|Yes| G\["Read sling:resourceType"]
G --> H\["Look for a servlet or script matching<br/>resourceType + selectors + extension + method<br/>— /apps first, then /libs"]
H --> I\{"Found?"}
I -->|Yes| J\["Use it"]
I -->|No| K\["Follow sling:resourceSuperType"]
K --> L\{"Found?"}
L -->|No| M\["Default servlets<br/>DefaultGetServlet / SlingPostServlet"]
⛶ Fullscreen
The single most important thing in that diagram is the top branch.

Path-bound servlets are checked first, in a separate registry, and they skip the resource resolution step entirely. That is not a quirk — it is the definition of path binding. And since resource resolution is where permissions are checked, skipping it is exactly why ACLs do not apply.

Being able to explain the security consequence as a consequence of the resolution order, rather than as an isolated fact, is what makes the answer sound like understanding.

3.2 Servlet versus script — the same slot
A useful reframing that impresses interviewers.

A resource-type servlet and an HTL script compete for the same position in resolution. Both answer "what renders this resource type with these selectors?"

resourceType = energy/components/categorylisting, selector = cards, extension = html

Candidates, in order:
/apps/energy/components/categorylisting/cards.html      ← an HTL script
a registered servlet with matching properties            ← your servletCopy
Copy
So you could implement Load More as either an HTL script or a servlet. The reason to choose a servlet is that the logic is substantial and belongs in testable Java rather than in a template.

"A resource-type servlet occupies the same slot in Sling's resolution as an HTL script — they're alternatives for the same job. I use a servlet when there's real logic that belongs in testable Java, and a script when it's genuinely presentational. Knowing they're interchangeable is useful, because it means a servlet with a selector doesn't need any special URL — it just uses the normal content URL with a selector on it."

3.3 What a servlet has access to
Because it takes SlingHttpServletRequest, a servlet gets the whole Sling context:

Resource resource = request.getResource();               // the resolved resource
ResourceResolver resolver = request.getResourceResolver(); // the user's session
RequestPathInfo info = request.getRequestPathInfo();     // selectors, suffix, extension
String param = request.getParameter("q");                // query/form parameterCopy
Copy
getResourceResolver() is important and easy to get wrong. It returns a resolver bound to the current user — anonymous on publish. So repository reads through it automatically respect that user's permissions.

That is usually what you want. If you need to read something the visitor cannot see, you need a service user — the pattern from file 01, with try-with-resources. But do that deliberately, because you are stepping outside the permission model on purpose.

And crucially: never close the request's resolver. You did not open it; Sling did, and Sling will close it. Closing it breaks the rest of the request.

That is a genuinely good detail — it inverts the usual "always close your resolver" rule, and knowing when the rule applies shows real understanding.

4 sections hidden in Learn mode
4\. Important Interview Questions · 5. Cross Questions — how this topic gets drilled · 6. Best Interview Answers · 7. Real Project Examples
Show
8\. Coding Examples
8.1 Resource-type servlet with all four methods
The reference example. Note the base class choice and the input handling.

package com.energy.core.servlets;

import com.energy.core.services.ProductDataService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.servlets.HttpConstants;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.servlets.annotations.SlingServletResourceTypes;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.Servlet;
import java.io.IOException;

/\*\*

- Demonstrates all four methods on ONE servlet for reference.
- In production these would normally be separate servlets with
- separate selectors -- this is deliberately a teaching example.
  \*/
  @Component(service = Servlet.class)
  @SlingServletResourceTypes(
  resourceTypes = "energy/components/productdetail",
  selectors = "specs",
  extensions = "json",
  methods = \{
  HttpConstants.METHOD\_GET,
  HttpConstants.METHOD\_POST,
  HttpConstants.METHOD\_PUT,
  HttpConstants.METHOD\_DELETE
  }
  )
  public class ProductSpecsServlet extends SlingAllMethodsServlet \{
  // SlingAllMethodsServlet because this one WRITES.
  // A read-only servlet would extend SlingSafeMethodsServlet.

  private static final long serialVersionUID = 1L;
  private static final Logger LOG = LoggerFactory.getLogger(ProductSpecsServlet.class);
  private static final ObjectMapper MAPPER = new ObjectMapper();

  /\*\*

  - @Reference works because a servlet IS an OSGi component.
  - MANDATORY by default -- if this service isn't registered,
  - this servlet never activates and the URL 404s with no error.
    \*/
    @Reference
    private ProductDataService productDataService;

  // ---------------- GET: read ----------------

  @Override
  protected void doGet(SlingHttpServletRequest request,
  SlingHttpServletResponse response)
  throws IOException \{

  ```
   // The resolved resource -- ACLs already applied by Sling,
   // because this is resource-type bound.
   Resource resource = request.getResource();

   // Read the suffix, never by parsing the URL yourself
   String productId = readProductId(request);
   if (productId == null) {
       sendError(response, SlingHttpServletResponse.SC_BAD_REQUEST,
                 "A product id is required in the suffix");
       return;
   }

   writeJson(response, productDataService.getSpecifications(productId));
  ```

  }

  // ---------------- POST: create / submit ----------------

  @Override
  protected void doPost(SlingHttpServletRequest request,
  SlingHttpServletResponse response)
  throws IOException \{

  ```
   String productId = request.getParameter("productId");
   if (productId == null || productId.trim().isEmpty()) {
       sendError(response, SlingHttpServletResponse.SC_BAD_REQUEST,
                 "productId is required");
       return;
   }

   try {
       productDataService.createSpecification(productId, request.getParameter("value"));
       response.setStatus(SlingHttpServletResponse.SC_CREATED);   // 201
       writeJson(response, java.util.Collections.singletonMap("status", "created"));

   } catch (Exception e) {
       // Log the detail; return something generic.
       // NEVER send a stack trace to the client -- it leaks internals.
       LOG.error("Failed to create specification for {}", productId, e);
       sendError(response, SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                 "Could not create the specification");
   }
  ```

  }

  // ---------------- PUT: replace ----------------

  @Override
  protected void doPut(SlingHttpServletRequest request,
  SlingHttpServletResponse response)
  throws IOException \{

  ```
   String productId = readProductId(request);
   if (productId == null) {
       sendError(response, SlingHttpServletResponse.SC_BAD_REQUEST, "Missing product id");
       return;
   }
   // PUT is idempotent: calling it twice has the same effect as once.
   productDataService.replaceSpecifications(productId, request.getReader());
   response.setStatus(SlingHttpServletResponse.SC_NO_CONTENT);    // 204
  ```

  }

  // ---------------- DELETE: remove ----------------

  @Override
  protected void doDelete(SlingHttpServletRequest request,
  SlingHttpServletResponse response)
  throws IOException \{

  ```
   String productId = readProductId(request);
   if (productId == null) {
       sendError(response, SlingHttpServletResponse.SC_BAD_REQUEST, "Missing product id");
       return;
   }
   // Also idempotent: deleting twice leaves it deleted.
   boolean removed = productDataService.deleteSpecifications(productId);
   response.setStatus(removed
           ? SlingHttpServletResponse.SC_NO_CONTENT      // 204
           : SlingHttpServletResponse.SC_NOT_FOUND);     // 404
  ```

  }

  // ---------------- helpers ----------------

  /\*\*

  - Reads the suffix. NEVER trust a value that arrived in a URL --
  - validate its shape before using it.
    \*/
    private String readProductId(SlingHttpServletRequest request) \{
    String suffix = request.getRequestPathInfo().getSuffix();
    if (suffix == null) \{
    return null;                       // getSuffix() is null when absent
    }
    String id = suffix.replaceFirst("^/", "").trim();
    // Whitelist the shape rather than blacklisting bad characters
    return id.matches("\[A-Za-z0-9\_-]\{1,64}") ? id : null;
    }

  private void writeJson(SlingHttpServletResponse response, Object payload)
  throws IOException \{
  // Set BOTH -- container defaults vary and a wrong charset
  // mangles non-ASCII characters.
  response.setContentType("application/json");
  response.setCharacterEncoding("UTF-8");
  // Serialise with a real library. String concatenation breaks
  // the moment a value contains a quote or apostrophe.
  MAPPER.writeValue(response.getWriter(), payload);
  }

  private void sendError(SlingHttpServletResponse response, int status, String message)
  throws IOException \{
  response.setStatus(status);
  writeJson(response, java.util.Collections.singletonMap("error", message));
  }
  }Copy
  Copy
  The eight decisions to be able to defend:

SlingAllMethodsServlet because it writes. Read-only would be the safe class.

@Reference works because a servlet is an OSGi component — and the comment names the failure mode.

The suffix is read via getRequestPathInfo(), never string-sliced.

Input is whitelisted by shape, not blacklisted — a URL value can be anything.

Errors log the detail and return something generic. A stack trace to the client is an information leak.

Content type and encoding are both set explicitly.

JSON is serialised by Jackson, not built with string concatenation.

Correct status codes — 201 for created, 204 for no content, 400 for bad input, 404, 500.

8.2 The cacheable Load More servlet — file 02's story in code
package com.energy.core.servlets;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.request.RequestDispatcherOptions;
import org.apache.sling.api.servlets.HttpConstants;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.servlets.annotations.SlingServletResourceTypes;
import org.osgi.service.component.annotations.Component;

import javax.servlet.Servlet;
import java.io.IOException;

/\*\*

- Serves additional card batches for the product listing.
-
- WHY RESOURCE TYPE + SELECTOR + SUFFIX, not a path + query parameter:
-
- The dispatcher builds its cache filename from the URL PATH.
- A selector and a suffix are part of the path, so
- ```
    /content/energy/global/en/products.cards.html/2
  ```
- is cached as a file, exactly like a page.
-
- A query parameter is not part of the path, so
- ```
    /bin/energy/cards?page=2
  ```
- goes through to publish on EVERY Load More click.
-
- Same data. One caches, the other doesn't.
-
- It also means content ACLs apply automatically, because Sling
- resolves a real resource before this code runs.
  \*/
  @Component(service = Servlet.class)
  @SlingServletResourceTypes(
  resourceTypes = "energy/components/categorylisting",
  selectors = "cards",
  extensions = "html",
  methods = HttpConstants.METHOD\_GET
  )
  public class CategoryCardsServlet extends SlingSafeMethodsServlet \{
  // SAFE methods class: this endpoint only reads.

  private static final long serialVersionUID = 1L;
  private static final int MAX\_BATCH = 20;

  @Override
  protected void doGet(SlingHttpServletRequest request,
  SlingHttpServletResponse response)
  throws IOException \{

  ```
   int batch = parseBatch(request.getRequestPathInfo().getSuffix());

   response.setContentType("text/html");
   response.setCharacterEncoding("UTF-8");

   // Return a RENDERED HTML FRAGMENT, not raw JSON.
   //
   // The first batch is server-rendered by HTL for SEO -- a crawler
   // will never click Load More. Later batches come from here. Two
   // separate templates for the same card drifted apart and the grid
   // visibly broke at the batch boundary, so we render through the
   // same script instead.
   RequestDispatcherOptions options = new RequestDispatcherOptions();
   options.setReplaceSelectors("cardsfragment." + batch);

   request.getRequestDispatcher(request.getResource(), options)
          .include(request, response);
  ```

  }

  /\*\*

  - NEVER trust a suffix value. Without clamping, someone requesting
  - batch 99999 makes the server build an enormous list.
    \*/
    private int parseBatch(String suffix) \{
    if (suffix == null) \{
    return 1;
    }
    try \{
    int value = Integer.parseInt(suffix.replace("/", "").trim());
    return Math.max(1, Math.min(value, MAX\_BATCH));
    } catch (NumberFormatException e) \{
    return 1;
    }
    }
    }Copy
    Copy
    8.3 A path-bound servlet — done correctly
    When path binding is genuinely justified, this is what it has to include.

package com.energy.core.servlets;

import com.energy.core.services.CrmService;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.servlets.annotations.SlingServletPaths;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.Servlet;
import java.io.IOException;

/\*\*

- Handles "request a quote" submissions, which go to an external CRM
- rather than to a repository node.
-
- PATH-BOUND IS JUSTIFIED HERE because there is genuinely no content
- context -- nothing is created in the repository, so there is no
- resource for a resource-type registration to attach to.
-
- TWO THINGS THIS COSTS US, both handled explicitly below:
-
- 1. NO CONTENT ACLs. Sling resolves no resource, so no permission
- ```
   check happens. Authorisation is entirely our responsibility.
  ```
-
- 2. DISPATCHER WHITELIST. /bin is denied by default, so this needs
- ```
   an explicit allow rule or it 404s on publish while working
  ```
- ```
   perfectly on author.
  ```

\*/
@Component(service = Servlet.class)
@SlingServletPaths("/bin/energy/quote-request")
public class QuoteRequestServlet extends SlingAllMethodsServlet \{

```
private static final long serialVersionUID = 1L;
private static final Logger LOG = LoggerFactory.getLogger(QuoteRequestServlet.class);
private static final int MAX_MESSAGE_LENGTH = 2000;

@Reference
private CrmService crmService;

@Override
protected void doPost(SlingHttpServletRequest request,
                      SlingHttpServletResponse response)
        throws IOException {

    // ---- AUTHORISATION: mandatory, because Sling did none ----
    if (!isAuthorised(request)) {
        // 403, and no detail about why -- don't help an attacker
        response.setStatus(SlingHttpServletResponse.SC_FORBIDDEN);
        return;
    }

    // ---- VALIDATION: everything from a form is untrusted ----
    String email   = request.getParameter("email");
    String message = request.getParameter("message");

    if (!isValidEmail(email)) {
        sendError(response, SlingHttpServletResponse.SC_BAD_REQUEST,
                  "A valid email address is required");
        return;
    }
    if (message == null || message.length() > MAX_MESSAGE_LENGTH) {
        sendError(response, SlingHttpServletResponse.SC_BAD_REQUEST,
                  "Message is missing or too long");
        return;
    }

    try {
        crmService.submitQuoteRequest(email, message);
        response.setStatus(SlingHttpServletResponse.SC_ACCEPTED);   // 202

    } catch (Exception e) {
        LOG.error("Quote submission failed", e);
        sendError(response, SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                  "Could not submit your request. Please try again.");
    }
}

/**
 * Our own permission check, since there is no resource to check.
 * What "authorised" means depends on the endpoint -- here the form
 * is public, so we verify the CSRF token rather than a user, which
 * stops another site posting on a visitor's behalf.
 */
private boolean isAuthorised(SlingHttpServletRequest request) {
    ResourceResolver resolver = request.getResourceResolver();
    // NOTE: do NOT close this resolver. Sling opened it and Sling
    // will close it -- closing it here breaks the rest of the request.
    String user = resolver.getUserID();
    LOG.debug("Quote request from user: {}", user);

    // AEM's CSRF filter handles the token check for us when enabled;
    // an internal-only endpoint would check group membership here.
    return true;
}

private boolean isValidEmail(String email) {
    return email != null && email.matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$");
}

private void sendError(SlingHttpServletResponse response, int status, String message)
        throws IOException {
    response.setStatus(status);
    response.setContentType("application/json");
    response.setCharacterEncoding("UTF-8");
    response.getWriter().write(
            "{\"error\":\"" + message.replace("\"", "'") + "\"}");
}
```

}Copy
Copy
And the dispatcher rule it requires:

# /bin is denied by default. Without this, the servlet works on

# author and returns 404 on the published site.

/0200 \{ /type "allow" /path "/bin/energy/quote-request" /method "POST" }Copy
Copy
Note the rule is as narrow as possible — one exact path, one method. Allowing /bin/\* for all methods would expose every path-bound servlet in the system, including ones you didn't write.

8.4 A Sling filter
package com.energy.core.filters;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.engine.EngineConstants;
import org.apache.sling.servlets.annotations.SlingServletFilter;
import org.apache.sling.servlets.annotations.SlingServletFilterScope;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.propertytypes.ServiceRanking;

import javax.servlet.\*;
import java.io.IOException;

/\*\*

- Adds security headers to every request.
-
- REQUEST scope = runs ONCE per request.
- COMPONENT scope would run for every component include, which for
- response headers would be pointless work repeated dozens of times.
  \*/
  @Component(service = Filter.class)
  @SlingServletFilter(scope = SlingServletFilterScope.REQUEST)
  @ServiceRanking(-700)   // ordering relative to other filters
  public class SecurityHeadersFilter implements Filter \{

  @Override
  public void doFilter(ServletRequest request, ServletResponse response,
  FilterChain chain) throws IOException, ServletException \{

  ```
   if (response instanceof SlingHttpServletResponse) {
       SlingHttpServletResponse slingResponse = (SlingHttpServletResponse) response;
       slingResponse.setHeader("X-Content-Type-Options", "nosniff");
       slingResponse.setHeader("X-Frame-Options", "SAMEORIGIN");
       slingResponse.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
   }

   // Keep filters LIGHT -- this runs on every single request.
   chain.doFilter(request, response);
  ```

  }

  @Override public void init(FilterConfig config) \{ }
  @Override public void destroy() \{ }
  }Copy
  Copy
  8.5 Unit testing a servlet
  package com.energy.core.servlets;

import com.energy.core.services.ProductDataService;
import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;

import static org.junit.jupiter.api.Assertions.\*;

@ExtendWith(AemContextExtension.class)
class ProductSpecsServletTest \{

```
private final AemContext context = new AemContext();
private ProductSpecsServlet servlet;

@BeforeEach
void setUp() {
    context.load().json("/servlets/content.json", "/content");
    context.registerService(ProductDataService.class,
            Mockito.mock(ProductDataService.class));
    servlet = context.registerInjectActivateService(new ProductSpecsServlet());
}

@Test
void rejectsARequestWithNoSuffix() throws Exception {
    context.currentResource("/content/product");
    // no suffix set

    servlet.doGet(context.request(), context.response());

    assertEquals(400, context.response().getStatus());
}

@Test
void rejectsAMalformedProductId() throws Exception {
    // Whitelist validation should reject path-traversal style input
    context.currentResource("/content/product");
    context.requestPathInfo().setSuffix("/../../etc/passwd");

    servlet.doGet(context.request(), context.response());

    assertEquals(400, context.response().getStatus());
}

@Test
void returnsJsonContentType() throws Exception {
    context.currentResource("/content/product");
    context.requestPathInfo().setSuffix("/TX-4000");

    servlet.doGet(context.request(), context.response());

    assertEquals(200, context.response().getStatus());
    assertTrue(context.response().getContentType().contains("application/json"));
}
```

}Copy
Copy
Note what is being tested: the failure paths. Missing input, malformed input, the content type. Testing the happy path proves the least — it is the bad input and the error handling that break in production, and for a public endpoint the malformed-input test is the one a security reviewer will look for.

9. Common Mistakes
   The mistake	What actually happens	The fix
   Path-bound servlet with no authorisation	Unauthenticated endpoint — anyone with the URL gets data	Register by resource type, or check permissions in code
   Path-bound servlet not whitelisted in the dispatcher	Works on author, 404 on publish	Add a narrow allow rule for that exact path and method
   /bin/\* allowed broadly in the dispatcher	Exposes every path-bound servlet, including ones you didn't write	One rule per path, per method
   Resource-type servlet with no selector	Takes over the page render for that resource type	Always use a selector
   Query parameters instead of a selector or suffix	Response isn't dispatcher-cacheable	Selector + suffix
   Not clamping a value from the URL	Someone requests page 99999 and the server builds a huge list	Parse, validate the shape, clamp
   SlingAllMethodsServlet for a read-only servlet	Advertises methods you didn't implement; a review finding	SlingSafeMethodsServlet
   Closing request.getResourceResolver()	Breaks the rest of the request — you didn't open it	Only close resolvers you opened yourself
   Leaking a service-user resolver you opened	Session leak, eventually memory exhaustion	try-with-resources
   Returning a stack trace to the client	Leaks internal paths, class names, versions	Log the detail, return something generic
   Building JSON with string concatenation	Breaks on the first quote or apostrophe in the data	Jackson or another serialiser
   Not setting content type and encoding	Wrong rendering, mangled non-ASCII characters	Set both explicitly
   Always returning 200	Clients can't distinguish success from failure	201, 204, 400, 403, 404, 500 as appropriate
   Parsing the URL with substring	Breaks as soon as a selector or suffix appears	getRequestPathInfo()
   Heavy logic inside a filter	Runs on every single request, so the cost multiplies	Move it to a servlet or service
   Forgetting the servlet is an OSGi component	A missing @Reference gives a silent 404	Check /system/console/components
10. Best Practices
    On registration. Default to resource type. Always include a selector so you don't shadow the page render. Use path binding only when there is genuinely no content context, and when you do, add the authorisation check and the dispatcher rule in the same change.

On base classes. SlingSafeMethodsServlet unless it writes. That is free documentation and a smaller surface.

On input. Everything from a URL or a form is untrusted. Validate by whitelisting the expected shape rather than blacklisting bad characters, and clamp anything numeric.

On output. Set content type and encoding explicitly. Serialise with a library. Use meaningful status codes. Never return a stack trace.

On resolvers. Never close the request's resolver. Always close one you opened yourself, with try-with-resources. Use a service user only when you genuinely need to read past the visitor's permissions, and check that the output doesn't leak what you just read.

On structure. Keep servlets thin — parse the request, call a service, write the response. Business logic belongs in an OSGi service where it is reusable and testable.

On caching. Decide cacheability before you write the endpoint. Selector and suffix if it should cache; accept that query parameters won't.

On filters. Only for genuinely cross-cutting concerns, REQUEST scope unless you specifically need per-include behaviour, and keep them light.

11. Debugging Tips
    The single most useful console for this topic is /system/console/servletresolver. You give it a URL and it tells you exactly which servlet or script will handle it — and which candidates it considered and rejected. That converts "why isn't my servlet called" from guesswork into one reading.

The four-step checklist, in order:

/system/console/servletresolver — what actually resolves for that URL?
/system/console/components — is the servlet component Active or Unsatisfied? A missing @Reference gives a silent 404.
Registration properties — do resource type, selector, extension and method all match?
Dispatcher — if path-bound and only failing on publish, check dispatcher.log to see whether the request reached AEM at all.
Reading the status code is a diagnostic in itself:

Status	What it tells you
404	No servlet matched — or the component is unsatisfied, or the dispatcher blocked it
405	A servlet was found, but that HTTP method isn't implemented — wrong base class, or methods doesn't include it
403	Permission denied — the resource ACL, or your own check
500	Your code threw — check error.log
200 with an empty body	The servlet ran but wrote nothing — usually an early return without a status
404 versus 405 is the most useful distinction here. 405 means the resolution worked and only the method is wrong, which eliminates three of the four checks above immediately.

Console	Answers
/system/console/servletresolver	Which servlet handles this URL
/system/console/components	Is the servlet component Active or Unsatisfied
/system/console/services	Is it registered under javax.servlet.Servlet
/system/console/slinglog	A DEBUG logger for just your servlet package
dispatcher.log	Did the request even reach AEM
Browser network tab	Status code, content type, response body
12\. Performance Optimization
Make it cacheable, and this dominates everything else. A resource-type servlet with a selector and suffix produces a path-based URL that the dispatcher caches as a file. The same endpoint with query parameters hits publish on every single request. On the Load More story that was the difference between a cached fragment and a page-tree traversal per click.

Keep the servlet thin. Parse, delegate to a service, write. Logic in a service can be cached at the service layer and reused by models and other servlets.

Never make a blocking external call without a timeout. Same rule as file 06 — a hung upstream consumes request threads until the instance stops responding.

Clamp everything. Limits, page sizes, batch numbers. An unbounded value from a URL is how someone makes your server do arbitrary amounts of work.

Return only what's needed. A JSON endpoint returning the full node tree when the client uses four fields wastes bandwidth and serialisation time.

Set cache headers deliberately, so the CDN can cache too, not just the dispatcher.

Keep filters minimal. A filter runs on every request, so a few milliseconds there is a few milliseconds on every page in the site.

2 sections hidden in Learn mode
13\. Real Production Scenarios · 14. Follow-up Questions
Show
15\. Comparison Tables
Path vs Resource Type — syllabus point 15

@SlingServletPaths	@SlingServletResourceTypes
URL	Fixed, e.g. /bin/energy/x	Content path + selector
Real resource behind it	No	Yes
Content ACLs	Not applied	Applied automatically
Authorisation	Your responsibility	Free
Dispatcher	Must be whitelisted	Already allowed
Cacheable	Rarely	Yes, with selector/suffix
Needs content to exist	No	Yes
Recommendation	Exception	Default
SlingSafeMethodsServlet vs SlingAllMethodsServlet — syllabus point 16

Safe	All
Methods	GET, HEAD, OPTIONS, TRACE	+ POST, PUT, DELETE
Changes state	No	Yes
Override	doGet, doHead	+ doPost, doPut, doDelete
Relationship	—	Extends the safe one
Use for	Read-only endpoints	Anything that writes
HTTP methods

Method	Purpose	Safe	Idempotent	Typical success status
GET	Read	Yes	Yes	200
POST	Create / submit	No	No	201 or 202
PUT	Replace	No	Yes	200 or 204
DELETE	Remove	No	Yes	204
Servlet vs Filter vs Sling Model

Sling Model	Servlet	Filter
Runs when	A page renders	A URL is requested directly	Every request
Purpose	Supply data to HTL	Handle a data request	Cross-cutting concerns
Injection	@OSGiService	@Reference	@Reference
Use for	Component logic	AJAX, form handlers, JSON	Headers, logging, redirects
Selector vs Suffix vs Query parameter (from file 01, applied here)

Selector	Suffix	Query param
Example	.cards.	.html/2	?page=2
Part of the path	Yes	Yes	No
Dispatcher-cacheable	Yes	Yes	No
Read with	getSelectors()	getSuffix()	getParameter()
Default servlets

DefaultGetServlet	SlingPostServlet
Handles	GET with no match	POST to a repository path
Produces	.json, .html, .xml	Creates/updates nodes
Known for	.infinity.json	Saving dialogs
Security concern	Can dump content	Anyone with write access can create nodes

6 sections hidden in Learn mode
16\. Memory Tricks · 17. Revision Notes · 18. Cheat Sheet · 19. Frequently Forgotten Things · 20. Final Interview Summary · 21. Mock Interview
Show
Next topic
08-HTL-Sightly — your syllabus points 17 and 18: how you call a Sling Model from HTL, every data-sly block element and what it's for, expression options and the display contexts, and why HTL deliberately can't do what JSP could.
