springthymeleafspring-thymeleaf

Thymeleaf does not use UTF-8 to transmit form data to rest endpoint


I am currently working on a Spring Boot project with Thymeleaf.

Using the following form should send the data to a REST endpoint:

<form th:action="@{/post/create}" method="post" th:object="${postDto}" accept-charset="UTF-8">
    <div class="mb-3">
        <label for="title" class="form-label">Titel</label>
        <input type="text" class="form-control" id="title" name="title" th:field="*{title}" required>
    </div>
    <div class="mb-3">
        <label for="content" class="form-label">Beschreibung</label>
        <textarea class="form-control" id="content" name="content" rows="4" th:field="*{content}" required></textarea>
    </div>
    <div class="mb-3">
        <label for="event" class="form-label">Event</label>
        <select class="form-control" id="event" name="event" th:field="*{eventId}">
            <option th:value="${0}">Kein Event</option>
            <option th:each="event : ${events}" th:value="${event.id}" th:text="${event.name} + ' - ' + ${event.getClassName()}"></option>
        </select>
    </div>
    <div class="mb-3">
        <label for="topic" class="form-label">Thema</label>
        <select class="form-control" id="topic" name="topic" th:field="*{topicId}">
            <option th:value="${0}">Kein Thema</option>
            <option th:each="topic : ${topics}" th:value="${topic.id}" th:text="${topic.name}"></option>
        </select>
    </div>
    <div class="mb-3">
        <label for="visibility" class="form-label">Sichtbarkeit</label>
        <select class="form-control" id="visibility" name="visibility" th:field="*{visibility}">
            <option th:value="${0}">Für alle sichtbar</option>
            <option th:each="role : ${roles}" th:value="${role.getVisibilityScore()}" th:text="${role.getVisibilityScore()} + ' - ' + ${role.name}"></option>
        </select>
    </div>
    <div class="modal-footer">
        <input type="hidden" name="_csrf" value="${_csrf.token}"/>
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
        <button type="submit" class="btn btn-primary">Absenden</button>
    </div>
</form>

The REST-Endpoint is receiving the data and should create a Post object in the ServiceImpl:

@PostMapping("/post/create")
    public String createPost(@ModelAttribute @Valid PostDto postDto,
                             BindingResult bindingResult,
                             Model model) {
        if (bindingResult.hasErrors()) {
            model.addAttribute(ERROR_MESSAGE_ATTRIBUTE, "Es gab Probleme, den Beitrag anzulegen. Versuche es erneut.");
            return TEMPLATE_LOCATION;
        }
        try {
            postService.savePost(postDto);
            model.addAttribute(SUCCESS_MESSAGE_ATTRIBUTE, "Beitrag wurde erstellt!");
        } catch (RuntimeException e) {
            model.addAttribute(ERROR_MESSAGE_ATTRIBUTE, e.getMessage());
        }
        return TEMPLATE_LOCATION;
    }

PostDto:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PostDto {
    @NotNull
    @NotEmpty
    private String title;

    @Lob
    @Column(length = 100000)
    @NotNull
    @NotEmpty
    private String content;

    private Long eventId;

    private Long topicId;

    @Min(0)
    @Max(100)
    @NotNull
    private Long visibility;
}

ServiceImpl:

@Override
    public void savePost(PostDto postDto) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User user = userService.findByMailAddress(authentication.getName());

        if (!permissionService.canCreatePosts(user)) {
            throw new InsufficientPermissionsException(INSUFFICIENT_PERMISSIONS_EXCEPTION);
        }

        Post post = Post.builder()
                .title(postDto.getTitle())
                .content(postDto.getContent())
                .visibility(postDto.getVisibility())
                .event(eventService.getById(postDto.getEventId()))
                .topic(topicService.getById(postDto.getTopicId()))
                .creator(user)
                .creationDate(LocalDateTime.now())
                .build();

        postRepository.save(post);
        topicService.mailToSubscribers(post.getTopic());
    }

Using the Spring Data JPA the object should get persisted in a database and users can read the post on the site.

After submitting the form, i logged the PostDto object and its already messed up and character like "ä", "ö", "ü" or other will be replaced with a "?".

I tried setting the following settings in the application.properties

server.servlet.encoding.charset=UTF-8
server.servlet.encoding.enabled=true
server.servlet.encoding.force=true
spring.thymeleaf.encoding=UTF-8

I found no solution in the web and used ChatGPT but got no awnser that fixed my problem.


Solution

  • I have implemented an AttributeConverter which was missing a StandardCharset. Thats why I had this issue.

    You can see the solution right here:

    @Component
    @Converter
    public class DataEncryptionConverter implements AttributeConverter<String, String> {
    
        private final KeyStoreManager keyStoreManager;
        private static final String ALGORITHM = "AES";
    
        @Autowired
        public DataEncryptionConverter(KeyStoreManager keyStoreManager) {
            this.keyStoreManager = keyStoreManager;
        }
    
        @Override
        public String convertToDatabaseColumn(String attribute) {
            try {
                Cipher cipher = Cipher.getInstance(ALGORITHM);
                cipher.init(Cipher.ENCRYPT_MODE, keyStoreManager.loadOrCreateSecretKey());
                byte[] encryptedData = cipher.doFinal(attribute.getBytes(StandardCharsets.UTF_8));
                return Base64.getEncoder().encodeToString(encryptedData);
            } catch (Exception e) {
                throw new RuntimeException("Encryption error", e);
            }
        }
    
        @Override
        public String convertToEntityAttribute(String dbData) {
            try {
                Cipher cipher = Cipher.getInstance(ALGORITHM);
                cipher.init(Cipher.DECRYPT_MODE, keyStoreManager.loadOrCreateSecretKey());
                byte[] decryptedData = cipher.doFinal(Base64.getDecoder().decode(dbData));
                return new String(decryptedData, StandardCharsets.UTF_8);
            } catch (Exception e) {
                throw new RuntimeException("Decryption error", e);
            }
        }
    }