Thymeleaf 405 Метод запроса POST не поддерживается

Итак, я создаю базовое приложение для социальных сетей для изучения ломбока и тимелеафа, и у меня возникают проблемы, когда я пытаюсь удалить запись из mongoDB. Мое приложение может загружать изображения нормально, но нажатие клавиши удаления отправит запрос POST вместо запроса DELETE.

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")
}

Thymeleaf 405 Метод запроса POST не поддерживается

Судя по моей отладке на этом снимке экрана, похоже, что метод формы определяет действие как «сообщение», хотя я указал «удалить»? Для меня не имеет смысла, зачем он это делает. Любые идеи поощряются.

Вы видите скрытое поле с name = "_method"? Это способ тимелеафа, говорящий о том, что нужно использовать другой метод, нежели пост.

mrkernelpanic 07.09.2018 08:23

Я имею в виду, что я указываю метод th: для удаления формы. Единственный метод публикации - вне таблицы. Вот почему странно появляться там, где они есть.

starrynights89 07.09.2018 08:34
0
2
1 440
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Мне удалось это исправить сегодня утром. Я меняю @DeleteMapping на моем HomeController на @PostMapping. Я не уверен на 100%, почему он решил проблему, я просто знаю, что это работает.

Спасибо еще раз!

Я думаю, вы немного ошибаетесь с HTTP и HTML. DELETE и PUT являются частью протокола HTTP.

До HTML 5 DELETE и PUT не поддерживались и все еще очень ограничены в зависимости от каждого браузера. Я подозреваю, что тимелеаф не поддерживает DELETE как метод формы, что заставляет его изменить его на метод POST.

Я предлагаю изменить ваш метод на POST и заставить ваш контроллер прослушивать запросы POST.

Контроллер, который прослушивает запросы HTTP DELETE, очень полезен, например, для RESTfull услуги.

Да, именно это я и сделал, отладив приложение и обнаружив, что HTML5 не поддерживает запросы DELETE. Таким образом, изменение моего контроллера на отображение POST привело к устранению проблемы.

starrynights89 09.09.2018 00:13

Для поддержки тега Th: method = "delete" вам потребуется HiddenHttpMethodFilter.

Так что добавь

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

вашему основному классу.

В Spring Boot HiddenHttpMethodFilter также можно включить с помощью следующего свойства: spring.mvc.hiddenmethod.filter.enabled=true

MartinBG 02.05.2020 21:34

Другие вопросы по теме