starrynights89
starrynights89

Reputation: 139

Thymeleaf 405 Request method 'POST' not supported

So I'm building a a basic social media application to learn lombok and thymeleaf, and I'm having issues when I try to delete a record out of mongoDB. My application can upload images just fine, but pressing the delete key will send a POST request instead of a DELETE request.

index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>Spring-a-Gram</title>
    <link rel="stylesheet" href="/main.css" />
</head>
<body>

<h1>Spring Boot - Social</h1>

<div>
    <table>
        <thead>
        <tr>
            <th>Id</th><th>Name</th><th>Image</th><th></th>
        </tr>
        </thead>
        <tbody>
        <tr th:each="image : ${images}">
            <td th:text="${image.id}" />
            <td th:text="${image.name}" />
            <td>
                <a th:href="@{'/images/' + ${image.name} + '/raw'}">
                    <img th:src="@{'/images/'+${image.name}+'/raw'}"
                         class="thumbnail" />
                </a>
            </td>
            <td>
                <form th:method="delete"
                      th:action="@{'/images/' + ${image.name}}">
                    <input type="submit" value="Delete" />
                </form>
            </td>
        </tr>
        </tbody>
    </table>

    <form method="post" enctype="multipart/form-data"
          action="/images">
        <p><input type="file" name="file" /></p>
        <p><input type="submit" value="Upload" /></p>
    </form>
</div>

</body>
</html>

HomeController.java

package com.alexander.springsocial;

import org.springframework.core.io.InputStreamResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.IOException;

/**
 * @author Alexander Hartson
 */
@Controller
public class HomeController {

    private static final String BASE_PATH = "/images";
    private static final String FILENAME = "{filename:.+}";

    private final ImageService imageService;

    public HomeController(ImageService imageService) {
        this.imageService = imageService;
    }

    @GetMapping("/")
    public Mono<String> index(Model model) {
        model.addAttribute("images", imageService.findAllImages());
        return Mono.just("index");
    }

    @GetMapping(value = BASE_PATH + "/" + FILENAME + "/raw",
        produces = MediaType.IMAGE_JPEG_VALUE)
    @ResponseBody
    public Mono<ResponseEntity<?>> oneRawImage(
            @PathVariable String filename) {
        return imageService.findOneImage(filename).map(resource -> {
            try {
                return ResponseEntity.ok()
                        .contentLength(resource.contentLength())
                        .body(new InputStreamResource(
                                resource.getInputStream()));
            } catch (IOException e) {
                return ResponseEntity.badRequest()
                        .body("Couldn't find " + filename +
                                " => " + e.getMessage());
            }
        });
    }

    @PostMapping(value = BASE_PATH)
    public Mono<String> createFile(@RequestPart(name = "file")
                                   Flux<FilePart> files) {
        return imageService.createImage(files)
                .then(Mono.just("redirect:/"));
    }

    @DeleteMapping(BASE_PATH + "/" + FILENAME)
    public Mono<String> deleteFile(@PathVariable String filename) {
        return imageService.deleteImage(filename)
                .then(Mono.just("redirect:/"));
    }
}

ImageService.java

package com.alexander.springsocial;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.UUID;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.stereotype.Service;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.FileSystemUtils;

/**
 * @author Alexander Hartson
 */
@Service
public class ImageService {

    public static String UPLOAD_ROOT = "upload-dir";

    // tag::injection[]
    private final ResourceLoader resourceLoader;

    private final ImageRepository imageRepository;

    public ImageService(ResourceLoader resourceLoader,
                        ImageRepository imageRepository) {
        this.resourceLoader = resourceLoader;
        this.imageRepository = imageRepository;
    }
    // end::injection[]

    // tag::1[]
    public Flux<Image> findAllImages() {
        return imageRepository.findAll()
                .log("findAll");
    }
    // end::1[]

    public Mono<Resource> findOneImage(String filename) {
        return Mono.fromSupplier(() ->
                resourceLoader.getResource(
                        "file:" + UPLOAD_ROOT + "/" + filename))
                .log("findOneImage");
    }

    // tag::2[]
    public Mono<Void> createImage(Flux<FilePart> files) {
        return files
                .log("createImage-files")
                .flatMap(file -> {
                    Mono<Image> saveDatabaseImage = imageRepository.save(
                            new Image(
                                    UUID.randomUUID().toString(),
                                    file.filename()))
                            .log("createImage-save");

                    Mono<Void> copyFile = Mono.just(
                            Paths.get(UPLOAD_ROOT, file.filename())
                                    .toFile())
                            .log("createImage-picktarget")
                            .map(destFile -> {
                                try {
                                    destFile.createNewFile();
                                    return destFile;
                                } catch (IOException e) {
                                    throw new RuntimeException(e);
                                }
                            })
                            .log("createImage-newfile")
                            .flatMap(file::transferTo)
                            .log("createImage-copy");

                    return Mono.when(saveDatabaseImage, copyFile)
                            .log("createImage-when");
                })
                .log("createImage-flatMap")
                .then()
                .log("createImage-done");
    }
    // end::2[]

    // tag::3[]
    public Mono<Void> deleteImage(String filename) {
        Mono<Void> deleteDatabaseImage = imageRepository
                .findByName(filename)
                .log("deleteImage-find")
                .flatMap(imageRepository::delete)
                .log("deleteImage-record");

        Mono<Object> deleteFile = Mono.fromRunnable(() -> {
            try {
                Files.deleteIfExists(
                        Paths.get(UPLOAD_ROOT, filename));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        })
                .log("deleteImage-file");

        return Mono.when(deleteDatabaseImage, deleteFile)
                .log("deleteImage-when")
                .then()
                .log("deleteImage-done");
    }
    // end::3[]

    /**
     * Pre-load some test images
     *
     * @return Spring Boot {@link CommandLineRunner} automatically
     *         run after app context is loaded.
     */
    @Bean
    CommandLineRunner setUp() throws IOException {
        return (args) -> {
            FileSystemUtils.deleteRecursively(new File(UPLOAD_ROOT));

            Files.createDirectory(Paths.get(UPLOAD_ROOT));

            FileCopyUtils.copy("Test file",
                    new FileWriter(UPLOAD_ROOT +
                            "/image1.jpg"));

            FileCopyUtils.copy("Test file2",
                    new FileWriter(UPLOAD_ROOT +
                            "/image2.jpg"));

            FileCopyUtils.copy("Test file3",
                    new FileWriter(UPLOAD_ROOT + "/image3.jpg"));
        };
    }
}

Gradle

buildscript {
    ext {
        springBootVersion = '2.0.4.RELEASE'
    }
    repositories {
        mavenCentral()
        maven { url "https://repo.spring.io/snapshot" }
        maven { url "https://repo.spring.io/milestone" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'com.alexander.spring-social'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
    maven { url "https://repo.spring.io/snapshot" }
    maven { url "https://repo.spring.io/milestone" }
    maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
}


dependencies {
    // tag::netty
    compile('org.springframework.boot:spring-boot-starter-webflux') {
        exclude group: 'org.springframework.boot',
        module: 'spring-boot-starter-reactor-netty'
    }
    compile('org.springframework.boot:spring-boot-starter-tomcat')
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    compile('org.springframework.boot:spring-boot-starter-data-mongodb-reactive')
    compile('org.synchronoss.cloud:nio-multipart-parser')
    // Specify lombok version
    compileOnly('org.projectlombok:lombok:1.18.2')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile("io.projectreactor:reactor-test")
}

enter image description here

From my debugging in this screenshot, it looks like the form method is defining the action as a "post" even though I specified "delete"? It doesn't make sense to me why it's doing this. Any ideas would be appreciated.

Upvotes: 0

Views: 2343

Answers (4)

Kris Kratz
Kris Kratz

Reputation: 81

Old thread, but it ranks well in Google.

In this case since you're using Webflux / Reactive, simply add: spring.webflux.hiddenmethod.filter.enabled=true to application.properties

This will get @DeleteMapping working correctly in your case.

Spring Boot 2.2 Release Notes · spring-projects/spring-boot Wiki

Upvotes: 1

starrynights89
starrynights89

Reputation: 139

I managed to fix it this morning. I change the @DeleteMapping on my HomeController to @PostMapping. I'm not 100% sure why it fixed the issue I just know that it works.

Thanks again!

Upvotes: 0

Olaf Schl&#252;ter
Olaf Schl&#252;ter

Reputation: 622

To support the Th:method="delete" tag you need a HiddenHttpMethodFilter.

So add

@Bean
HiddenHttpMethodFilter hiddenHttpMethodFilter() {
    return new HiddenHttpMethodFilter();
}

to your main class.

Upvotes: 1

Casper
Casper

Reputation: 303

I think you are mistaking HTTP and HTML a little bit. DELETE and PUT are both part of the HTTP protocol.

Until HTML 5 DELETE and PUT were not supported and it is still very limited depending on each browser. What I suspect is that thymeleaf does not support DELETE as a form method which is causing it to change it to the POST method.

I would suggest to change your method to POST and make your controller listen for POST requests.

A controller that listens for HTTP DELETE requests is very useful for e.g. RESTfull services.

Upvotes: 3

Related Questions