-1

Spring boot application multipart get file doesn't working in production environment

I have a spring boot mvc web application with thymeleaf. So everything is working normally in local pc but in production environment (remote server) trying to view and download the file the url prefix is sticking localhost:8080/file-url/view not example.az/file-url/view. My appication server is tomcat 9.0.71 version in linux server.

So here is the code snippet

Controller class

@Controller
public class FileBBController {

    private final CategoryBBService categoryBBService;

    public FileBBController(CategoryBBService categoryBBService) {
        this.categoryBBService = categoryBBService;
    }

    @GetMapping("/files/bb/new")
    public String newFile() {
        return "legislation/bb/upload_form";
    }

    @PostMapping(value = "/files/bb/upload")
    public String uploadFile(@RequestParam("file") MultipartFile file, Model model) {
        String message;
        try {
            categoryBBService.storeFile(file);
            message = "Fayl bazaya müvəfəqiyyətlə yükləndi: " + file.getOriginalFilename();
            model.addAttribute("message", message);
            Thread.sleep(4000);
        } catch (Exception e) {
            message = "Diqqət bir fayl seçməlisiniz!";
            model.addAttribute("message", message);
        }
        return "redirect:/bb/files";
    }

    @GetMapping("/bb/files")
    public String getFiles(Model model) {
        String keyword = null;
        return getOnePage(1, model, "createdAt", "desc", keyword);
    }

    @GetMapping("/files/{id}")
    public ResponseEntity<Resource> getFile(@PathVariable String id) {
        CategoryBB fileDB = categoryBBService.getFile(id);

        return ResponseEntity.ok().contentType(MediaType.APPLICATION_PDF)
                .header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION,
                        "attachment; filename=\"" + fileDB.getName() + "\"")
                .body(new ByteArrayResource(fileDB.getData()));
    }

    @GetMapping("/bb/files/page/{pageNo}")
    public String getOnePage(@PathVariable("pageNo") int currentPage, Model model,
                             @RequestParam(value = "sortField", required = false) String sortField,
                             @RequestParam(value = "sortDir", required = false) String sortDir,
                             @RequestParam(value = "keyword", required = false) String keyword
    ) {
        Page<ResponseFile> page = categoryBBService.findPaginated(currentPage, PAGE_SIZE, sortField, sortDir, keyword).map(dbFile -> {
            String fileDownloadUri = ServletUriComponentsBuilder.
                    fromCurrentContextPath()
                    .path("/files/")
                    .path(dbFile.getId())
                    .toUriString();
            System.out.println(fileDownloadUri);
            return new ResponseFile(dbFile.getName(),
                    fileDownloadUri,
                    dbFile.getType(),
                    FileUtils.byteCountToDisplaySize(dbFile.getData().length),
                    dbFile.getCreatedAt());
        });
        List<ResponseFile> files = page.getContent();
        model.addAttribute("currentPage", currentPage);
        model.addAttribute("totalPages", page.getTotalPages());
        model.addAttribute("totalElements", page.getTotalElements());
        model.addAttribute("sortField", sortField);
        model.addAttribute("sortDir", sortDir);
        model.addAttribute("reverseSortDir", sortDir.equals("asc") ? "desc" : "asc");
        model.addAttribute("keyword", keyword);
        model.addAttribute("files", files);

        return "legislation/bb/files";
    }

    @RequestMapping(value = "/files/{id}/bb/delete", method = RequestMethod.GET)
    public String deleteFile(@PathVariable("id") String id) {
        categoryBBService.deleteFileById(id);
        return "redirect:/bb/files";
    }

    @GetMapping("/files/{id}/bb/view")
    public void viewFile(@PathVariable String id, HttpServletResponse resp) throws IOException {
        CategoryBB file = categoryBBService.getFile(id);
        byte[] byteArray = file.getData();
        resp.setContentLength(byteArray.length);
        try (OutputStream os = resp.getOutputStream()) {
            os.write(byteArray, 0, byteArray.length);
        }
    }

}

Service class

@Service
public class CategoryBBService {

    private final CategoryBBRepository categoryBBRepository;

    public CategoryBBService(CategoryBBRepository categoryBBRepository) {
        this.categoryBBRepository = categoryBBRepository;
    }

    public void storeFile(MultipartFile file) throws IOException {

        String fileName = StringUtils.getFilename(file.getOriginalFilename());
        if (StringUtils.endsWithIgnoreCase(fileName, ".pdf")) {
            CategoryBB fileDB = new CategoryBB(fileName, file.getContentType(), file.getBytes());
            categoryBBRepository.save(fileDB);
        } else {
            throw new RuntimeException("Wrong file type uploaded");
        }
    }

    public CategoryBB getFile(String id) {
        return categoryBBRepository.findById(id).orElse(null);
    }

    public Page<CategoryBB> findPaginated(int pageNo, int pageSize,
                                          String sortField,
                                          String sortDirection,
                                          String keyword) {
        Sort sort = SortUtil.sorting(sortDirection, sortField);
        Pageable pageable = PageRequest.of(pageNo - 1, pageSize, sort);
        if (keyword != null) {
            return categoryBBRepository.findAll(keyword, pageable);
        }
        return categoryBBRepository.findAll(pageable);
    }

    public void deleteFileById(String id) {
        categoryBBRepository.deleteById(id);
    }

}

Entity class

@Entity
@Table(name = "category_bb")
public class CategoryBB extends FileDB {

    public CategoryBB() {
    }

    public CategoryBB(String name, String type, byte[] data) {
        super(name, type, data);
    }

}

Super class for Entity

@MappedSuperclass
public abstract class FileDB implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(name = "uuid", strategy = "uuid2")
    private String id;
    private String name;
    private String type;

    @Column(name = "file_url")
    private String fileUrl;
    @Lob
    private byte[] data;

    @CreationTimestamp
    @Column(name = "created_at")
    private LocalDateTime createdAt;

    public FileDB() {
    }

    public FileDB(String name, String type, byte[] data) {
        this.name = name;
        this.type = type;
        this.data = data;
    }

    public FileDB(String name, String type, String fileUrl, byte[] data) {
        this.name = name;
        this.type = type;
        this.fileUrl = fileUrl;
        this.data = data;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public byte[] getData() {
        return data;
    }

    public void setData(byte[] data) {
        this.data = data;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }

    public String getFileUrl() {
        return fileUrl;
    }

    public void setFileUrl(String fileUrl) {
        this.fileUrl = fileUrl;
    }

ResponseFile class

public class ResponseFile {
    private String name;
    private String url;
    private String type;
    private String size;

    public ResponseFile() {
    }

    public ResponseFile(String name, String url, String type, String size, LocalDateTime createdAt) {
        this.name = name;
        this.url = url;
        this.type = type;
        this.size = size;
        this.createdAt = createdAt;
    }

    private LocalDateTime createdAt;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getSize() {
        return size;
    }

    public void setSize(String size) {
        this.size = size;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }

}

files.html

<div th:if="${files.size() > 0}" class="table-responsive">
        <table class="table table-hover table-bordered border-primary">
            <thead class="thead-light table-secondary">
            <tr class="text-center">
                <th scope="col">№</th>
                <th scope="col">Faylın adı</th>
                <th scope="col">Faylı yüklə</th>
                <th scope="col">Fayla bax</th>
                <th scope="col">Ölçüsü</th>
                <th sec:authorize="hasRole('ADMIN')" scope="col">Actions</th>
                <th scope="col">
                    <a style="text-decoration: none; color: black"
                       th:href="@{'/bb/files/page/' + ${currentPage} + '?sortField=createdAt&sortDir=' + ${reverseSortDir} + ${keyword != null ? '&keyword=' + keyword : ''}}">
                        Tarix
                    </a>
                </th>
            </tr>
            </thead>
            <tbody class="table-group-divider">
            <tr th:each="file, fileStat : ${files}">
                <td class="text-center" th:text="${fileStat.count}"></td>
                <td th:text="${file.name}"></td>

                <td class="text-center"><a th:href="@{${file.url}}"><i class="fa-solid fa-cloud-arrow-down fa-lg"></i></a></td>
                <td class="text-center"><a target="_blank" th:href="@{${file.url} + '/bb/view/'}"><i class="fa fa-thin fa-eye fa-lg"></i></a></td>

                <td th:text="${file.size}"></td>
                <td th:text="${#temporals.format(file.createdAt, 'dd-MM-yyyy, HH:mm')}"></td>
                <td sec:authorize="hasRole('ADMIN')" class="text-center">
                    <a th:href="@{${file.url} + '/bb/delete/'}" th:fileName="${file.name}" style="border: none; background: none"
                      id="btnDelete" class="btn-delete" data-bs-toggle="modal" data-bs-target="#exampleModal">
                        <i class="fa-solid fa-trash fa-xl" style="color: red"></i>
                    </a>
                </td>
            </tr>
            </tbody>
        </table>
    </div>

When I click the buttton "Fayla bax (view file)" and "Faylı yüklə(download file)" it's prefix comming "http://localhost:8080/files/c9376e46-55cb-4cf5-beab-ba886bb23c5f/bb/view/" something like that. Would you help me solve this issue ?

enter image description here

enter image description here

  • Is there some infrastructure before your spring boot application (e.g. apache server, nginx, ...)? At least try to define some property which should handle forwarding `server.forward-headers-strategy=NATIVE`. Maybe [this](https://stackoverflow.com/questions/68318269/spring-server-forward-headers-strategy-native-vs-framework) question helps you. – bilak Feb 24 '23 at 07:16
  • The way `ServletUriComponentsBuilder.fromCurrentContextPath()` works is that it obtains the hostname from the `Host` header. So I assume you have some reverse proxy (Apache, nginx, ...) runnning in front of your application that does not forward the `Host` header. This means your question is unanswerable as long as we don't know how your application is running. – g00glen00b Feb 24 '23 at 07:48
  • @bilak so sorry i understood you but before i forgot my screen shot to drag here can you view the pictures again in above? – Elkhan Ismayilov Feb 24 '23 at 07:50
  • @g00glen00b no I did it as a said you in early I think it's nope – Elkhan Ismayilov Feb 24 '23 at 07:52
  • @ElkhanIsmayilov I still don't know if you have there some reverse proxy running or if that's just your spring boot app. I understand what your problem is but without knowing more nobody is able to help you. If you are running just spring-boot app, try to change the property which I sent you in my first comment and play with it. If `NATIVE` doesn't work try `FRAMEWORK`. – bilak Feb 24 '23 at 07:55
  • @bilak it's production based already but in locally everithing wirking like a charm – Elkhan Ismayilov Feb 24 '23 at 07:57
  • What is the value of `file.url` in your database? There is no code showing how you fill this field, but it seems to me like you have in this field url starting with `http://localhost:8080`. If you start this url from `/files/...` then the host part will be resolved by the browser – dey Feb 24 '23 at 08:01
  • @bilak wooooow you saved my day. many many thank you so much so I added server.forward-headers-strategy=framework my application.properties and done again thank's dude – Elkhan Ismayilov Feb 24 '23 at 08:02

1 Answers1

0

based on comments:
If you are using just spring-boot application without any reverse proxy (such as apache, nginx) try to configure spring's property server.forward-headers-strategy to either native or framework and it should resolve the issue.

bilak
  • 4,526
  • 3
  • 35
  • 75