CtrlK
BlogDocsLog inGet started
Tessl Logo
PluginPublic

adobe/aem-cloud-service

All AEM as a Cloud Service skills: component development, Workflow model design, development, triggering, launchers, debugging, and triaging, Dispatcher config authoring, advisory, incident response, performance tuning, and security hardening, Rapid Development Environment (RDE) deployment, log inspection, snapshots, and troubleshooting via `aio aem rde`, Jackrabbit Oak query/index tuning (property, Lucene index definitions), and extracting/inventorying JCR queries (XPath, SQL2, QueryBuilder) across a codebase.

Quality

85%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

sling-servlet-standards.mdskills/create-component/references/

Sling Servlet Standards for AEM Components

Placeholders: [project] = project name, [package] = Java package (from aem-conventions.md)

Table of Contents


When to Use Servlets

Component BehaviorServlet Needed?
Static content from dialogNo
Content from JCR (author-configured)No
Real-time data from external APIYes
Form submission with validationYes
Async content loadingYes
Search results based on user queryYes

File Location

core/src/main/java/[package-path]/servlets/{ComponentName}Servlet.java
core/src/test/java/[package-path]/servlets/{ComponentName}ServletTest.java

JSON Library - Use javax.json

Always use javax.json - guaranteed available in AEM, no dependency conflicts.

import javax.json.Json;
import javax.json.JsonObjectBuilder;
import javax.json.JsonArrayBuilder;
import javax.json.JsonReader;

// Build JSON
JsonObjectBuilder result = Json.createObjectBuilder();
result.add("success", true);
result.add("data", Json.createObjectBuilder().add("key", "value"));
response.getWriter().write(result.build().toString());

// Parse JSON
try (JsonReader reader = Json.createReader(new StringReader(jsonString))) {
    JsonObject obj = reader.readObject();
}

GET Servlet Pattern

package [package].servlets;

import java.io.IOException;
import javax.json.Json;
import javax.json.JsonObjectBuilder;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
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;

@Component(service = Servlet.class)
@SlingServletResourceTypes(
    resourceTypes = "[project]/components/{component-name}",
    methods = HttpConstants.METHOD_GET,
    extensions = "json",
    selectors = "data"
)
public class ComponentNameServlet extends SlingSafeMethodsServlet {

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

        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        JsonObjectBuilder result = Json.createObjectBuilder();
        try {
            result.add("success", true);
            result.add("data", Json.createObjectBuilder().add("key", "value"));
        } catch (Exception e) {
            response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            result.add("success", false);
            result.add("error", "An error occurred");
        }
        response.getWriter().write(result.build().toString());
    }
}

URL Pattern: /content/mysite/page/jcr:content/component.data.json


POST Servlet Pattern

package [package].servlets;

import java.io.IOException;
import javax.json.Json;
import javax.json.JsonObjectBuilder;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
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;

@Component(service = Servlet.class)
@SlingServletResourceTypes(
    resourceTypes = "[project]/components/{component-name}",
    methods = HttpConstants.METHOD_POST,
    extensions = "json",
    selectors = "submit"
)
public class ComponentNameServlet extends SlingAllMethodsServlet {

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

        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        JsonObjectBuilder result = Json.createObjectBuilder();
        try {
            String name = request.getParameter("name");
            // Validate and process...
            result.add("success", true);
            result.add("message", "Submitted successfully");
        } catch (IllegalArgumentException e) {
            response.setStatus(SlingHttpServletResponse.SC_BAD_REQUEST);
            result.add("success", false);
            result.add("error", e.getMessage());
        }
        response.getWriter().write(result.build().toString());
    }
}

JSON Response Structure

// Success
{ "success": true, "data": { ... } }

// Error
{ "success": false, "error": "User-friendly message" }
Status CodeWhen to Use
200Success
400Invalid input
403Access denied
404Not found
500Server error

Security Checklist

  • Input validation: Validate and sanitize all parameters
  • CSRF protection: AEM's CSRFFilter handles form submissions automatically
  • Error messages: Never expose stack traces or internal paths
  • Permissions: Use resource-based binding (respects ACLs)

Frontend Integration

HTL - Expose resource path:

<div data-cmp-is="componentname" data-resource-path="${resource.path}">

JavaScript - Fetch pattern:

var url = element.dataset.resourcePath + '.data.json';
fetch(url, { method: 'GET', credentials: 'same-origin' })
    .then(function(response) { return response.json(); })
    .then(function(result) {
        if (result.success) { /* handle data */ }
        else { /* handle error */ }
    });

CSRF Token Handling

POST servlets are protected by AEM's CSRF filter. The frontend must obtain a token and include it with every POST request. GET requests do not require CSRF tokens.

Step 1 — Fetch the CSRF token:

function getCsrfToken() {
    return fetch('/libs/granite/csrf/token.json', {
        credentials: 'same-origin'
    })
    .then(function(response) { return response.json(); })
    .then(function(data) { return data.token; });
}

Step 2 — Submit a POST request with the token:

getCsrfToken().then(function(token) {
    fetch(element.dataset.resourcePath + '.submit.json', {
        method: 'POST',
        credentials: 'same-origin',
        headers: {
            'CSRF-Token': token,
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: 'title=' + encodeURIComponent(title)
    })
    .then(function(response) { return response.json(); })
    .then(function(result) {
        if (result.success) { /* handle success */ }
        else { /* handle error */ }
    });
});

Alternative — Send the token as a form parameter:

var body = ':cq_csrf_token=' + encodeURIComponent(token)
         + '&title=' + encodeURIComponent(title);

Note: GET servlets do not need CSRF protection — only POST (and other state-changing methods) are filtered.


Unit Test Pattern

package [package].servlets;

import static org.junit.jupiter.api.Assertions.*;
import java.io.StringReader;
import javax.json.Json;
import javax.json.JsonObject;
import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest;
import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;

@ExtendWith(AemContextExtension.class)
class ComponentNameServletTest {

    private final AemContext context = new AemContext();

    @Test
    void testDoGet_Success() throws Exception {
        ComponentNameServlet servlet = new ComponentNameServlet();
        context.create().resource("/content/test", "sling:resourceType", "[project]/components/name");
        context.currentResource("/content/test");

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

        // IMPORTANT: Use startsWith() for content type (see note below)
        assertTrue(context.response().getContentType().startsWith("application/json"));
        
        JsonObject result = Json.createReader(
            new StringReader(context.response().getOutputAsString())).readObject();
        assertTrue(result.getBoolean("success"));
    }
}

Content Type Assertion - Critical Note

NEVER use exact match for content type in servlet tests:

// WRONG - Will fail because mock includes charset
assertEquals("application/json", context.response().getContentType());

// CORRECT - Use startsWith() to handle charset suffix
assertTrue(context.response().getContentType().startsWith("application/json"));

The MockSlingHttpServletResponse.getContentType() returns "application/json;charset=UTF-8" when both content type and character encoding are set.


Quick Reference

AttributeGET ServletPOST Servlet
Parent classSlingSafeMethodsServletSlingAllMethodsServlet
MethodHttpConstants.METHOD_GETHttpConstants.METHOD_POST
Typical selectordata, searchsubmit
Use caseRead dataWrite/submit data

Anti-Patterns to Avoid

Anti-PatternWhy It's BadBetter Approach
Path-based binding for component servletsBypasses ACLs, requires dispatcher configUse @SlingServletResourceTypes
Exposing stack tracesSecurity risk, reveals internal detailsLog server-side, return generic message
Hardcoded content pathsBreaks in different environmentsUse resource-relative paths
No input validationXSS, injection vulnerabilitiesValidate and sanitize all input
Synchronous blocking callsPoor performanceUse async patterns where possible
Ignoring HTTP status codesClients can't handle errors properlyReturn appropriate status codes
No CSRF protection on POSTVulnerable to CSRF attacksUse Granite CSRF framework
Returning sensitive dataPrivacy/security issuesOnly return necessary data

Checklist

Before Creating a Servlet

  • Confirmed servlet is needed (can't use Sling Model + HTL alone)
  • Chose resource-based binding (not path-based)
  • Identified correct HTTP method (GET for read, POST for write)
  • Determined selector and extension

Servlet Implementation

  • Package is [package].servlets
  • Class name follows {ComponentName}Servlet convention
  • @SlingServletResourceTypes uses correct resourceTypes
  • Correct parent class (SlingSafeMethodsServlet or SlingAllMethodsServlet)
  • Response content type set (application/json)
  • Response character encoding set (UTF-8)
  • Standard JSON response structure used
  • Proper HTTP status codes returned
  • Uses javax.json (not GSON)

Security

  • Input validated and sanitized
  • Error messages don't expose internals
  • CSRF protection for POST requests
  • Permission checks where needed

Frontend Integration

  • HTL exposes data-resource-path
  • JavaScript uses correct URL pattern (path.selector.extension)
  • Error handling with user feedback
  • Loading states implemented

Testing

  • Unit test file created
  • Success scenario tested
  • Error scenarios tested
  • Validation tested

skills

README.md

tile.json