diff --git a/Api/build.gradle b/Api/build.gradle index 93615c1b..a2abfcf6 100644 --- a/Api/build.gradle +++ b/Api/build.gradle @@ -10,7 +10,6 @@ repositories { } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security' // swagger diff --git a/Api/src/main/java/allchive/server/api/chore/controller/ChoreController.java b/Api/src/main/java/allchive/server/api/chore/controller/ChoreController.java new file mode 100644 index 00000000..4608f0c7 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/chore/controller/ChoreController.java @@ -0,0 +1,30 @@ +package allchive.server.api.chore.controller; + + +import allchive.server.api.archiving.model.dto.request.UpdateArchivingRequest; +import allchive.server.domain.domains.archiving.domain.enums.Category; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/chore") +@RequiredArgsConstructor +@Slf4j +@Tag(name = "etc. [chore]") +public class ChoreController { + @Operation(hidden = true) + @PostMapping(value = "error/{test}") + public void errorExample( + @RequestBody UpdateArchivingRequest updateArchivingRequest, + @RequestParam("category") Category category, + @PathVariable("test") Long test) { + throw new RuntimeException(); + } + + @Operation(hidden = true) + @GetMapping(value = "health") + public void errorExample() {} +} diff --git a/Api/src/main/java/allchive/server/api/common/filter/MultiReadInputStream.java b/Api/src/main/java/allchive/server/api/common/filter/MultiReadInputStream.java new file mode 100644 index 00000000..f75f1c7a --- /dev/null +++ b/Api/src/main/java/allchive/server/api/common/filter/MultiReadInputStream.java @@ -0,0 +1,61 @@ +package allchive.server.api.common.filter; + + +import com.amazonaws.util.IOUtils; +import java.io.*; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + +public class MultiReadInputStream extends HttpServletRequestWrapper { + private ByteArrayOutputStream cachedBytes; + + public MultiReadInputStream(HttpServletRequest request) { + super(request); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + if (cachedBytes == null) cacheInputStream(); + return new CachedServletInputStream(cachedBytes.toByteArray()); + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + private void cacheInputStream() throws IOException { + cachedBytes = new ByteArrayOutputStream(); + IOUtils.copy(super.getInputStream(), cachedBytes); + } + + private static class CachedServletInputStream extends ServletInputStream { + private final ByteArrayInputStream buffer; + + public CachedServletInputStream(byte[] contents) { + this.buffer = new ByteArrayInputStream(contents); + } + + @Override + public int read() { + return buffer.read(); + } + + @Override + public boolean isFinished() { + return buffer.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener listener) { + throw new RuntimeException("Not implemented"); + } + } +} diff --git a/Api/src/main/java/allchive/server/api/common/filter/MultiReadInputStreamFilter.java b/Api/src/main/java/allchive/server/api/common/filter/MultiReadInputStreamFilter.java new file mode 100644 index 00000000..76bf63e7 --- /dev/null +++ b/Api/src/main/java/allchive/server/api/common/filter/MultiReadInputStreamFilter.java @@ -0,0 +1,28 @@ +package allchive.server.api.common.filter; + + +import java.io.IOException; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; + +@Component +public class MultiReadInputStreamFilter implements Filter { + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + MultiReadInputStream multiReadRequest = + new MultiReadInputStream((HttpServletRequest) request); + chain.doFilter(multiReadRequest, response); + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + Filter.super.init(filterConfig); + } + + @Override + public void destroy() { + Filter.super.destroy(); + } +} diff --git a/Api/src/main/java/allchive/server/api/common/util/UrlUtil.java b/Api/src/main/java/allchive/server/api/common/util/UrlUtil.java index 2ffe0bbb..5ad6489f 100644 --- a/Api/src/main/java/allchive/server/api/common/util/UrlUtil.java +++ b/Api/src/main/java/allchive/server/api/common/util/UrlUtil.java @@ -29,6 +29,9 @@ public static String toAssetUrl(String key) { } public static String convertUrlToKey(String url) { + if (url == null) { + return ""; + } if (url.equals("")) { return ""; } diff --git a/Api/src/main/java/allchive/server/api/config/GlobalExceptionHandler.java b/Api/src/main/java/allchive/server/api/config/GlobalExceptionHandler.java index db9dd5b0..9d4d500c 100644 --- a/Api/src/main/java/allchive/server/api/config/GlobalExceptionHandler.java +++ b/Api/src/main/java/allchive/server/api/config/GlobalExceptionHandler.java @@ -25,6 +25,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import org.springframework.web.util.ContentCachingRequestWrapper; @Slf4j @RestControllerAdvice @@ -98,9 +99,11 @@ public ResponseEntity handleBaseErrorException( @ExceptionHandler(BaseDynamicException.class) public ResponseEntity BaseDynamicExceptionHandler( BaseDynamicException e, HttpServletRequest request) { + final ContentCachingRequestWrapper cachedRequest = + new ContentCachingRequestWrapper(request); ErrorResponse errorResponse = ErrorResponse.from(ErrorReason.of(e.getStatus(), e.getCode(), e.getMessage())); - Event.raise(SlackErrorEvent.from(e)); + Event.raise(SlackErrorEvent.of(e, cachedRequest)); return ResponseEntity.status(HttpStatus.valueOf(e.getStatus())).body(errorResponse); } @@ -109,10 +112,12 @@ public ResponseEntity BaseDynamicExceptionHandler( protected ResponseEntity handleException( Exception e, HttpServletRequest request) { log.error("Exception", e); + final ContentCachingRequestWrapper cachedRequest = + new ContentCachingRequestWrapper(request); final GlobalErrorCode globalErrorCode = GlobalErrorCode._INTERNAL_SERVER_ERROR; final ErrorReason errorReason = globalErrorCode.getErrorReason(); final ErrorResponse errorResponse = ErrorResponse.from(errorReason); - Event.raise(SlackErrorEvent.from(e)); + Event.raise(SlackErrorEvent.of(e, cachedRequest)); return ResponseEntity.status(INTERNAL_SERVER_ERROR).body(errorResponse); } } diff --git a/Api/src/main/java/allchive/server/api/config/security/SecurityConfig.java b/Api/src/main/java/allchive/server/api/config/security/SecurityConfig.java index 40746326..19691971 100644 --- a/Api/src/main/java/allchive/server/api/config/security/SecurityConfig.java +++ b/Api/src/main/java/allchive/server/api/config/security/SecurityConfig.java @@ -57,6 +57,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { } http.authorizeRequests() + .mvcMatchers("/chore/**") + .permitAll() .antMatchers(SwaggerPatterns) .permitAll() .mvcMatchers("/auth/oauth/**") diff --git a/Core/build.gradle b/Core/build.gradle index f67e5636..d633c34e 100644 --- a/Core/build.gradle +++ b/Core/build.gradle @@ -5,6 +5,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.2' + api 'org.springframework.boot:spring-boot-starter-web' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' diff --git a/Core/src/main/java/allchive/server/core/event/events/slack/SlackErrorEvent.java b/Core/src/main/java/allchive/server/core/event/events/slack/SlackErrorEvent.java index 0aea123b..77f8bacb 100644 --- a/Core/src/main/java/allchive/server/core/event/events/slack/SlackErrorEvent.java +++ b/Core/src/main/java/allchive/server/core/event/events/slack/SlackErrorEvent.java @@ -3,17 +3,21 @@ import lombok.Builder; import lombok.Getter; +import org.springframework.web.util.ContentCachingRequestWrapper; @Getter public class SlackErrorEvent { private Exception exception; + private ContentCachingRequestWrapper cachedRequest; @Builder - private SlackErrorEvent(Exception exception) { + private SlackErrorEvent(Exception exception, ContentCachingRequestWrapper cachedRequest) { this.exception = exception; + this.cachedRequest = cachedRequest; } - public static SlackErrorEvent from(Exception exception) { - return SlackErrorEvent.builder().exception(exception).build(); + public static SlackErrorEvent of( + Exception exception, ContentCachingRequestWrapper cachedRequest) { + return SlackErrorEvent.builder().exception(exception).cachedRequest(cachedRequest).build(); } } diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/slack/SlackMessageGenerater.java b/Infrastructure/src/main/java/allchive/server/infrastructure/slack/SlackMessageGenerater.java index 4fb99e08..8007019f 100644 --- a/Infrastructure/src/main/java/allchive/server/infrastructure/slack/SlackMessageGenerater.java +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/slack/SlackMessageGenerater.java @@ -14,13 +14,17 @@ import com.slack.api.model.block.composition.TextObject; import com.slack.api.webhook.Payload; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import javax.servlet.ServletInputStream; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import org.springframework.util.StreamUtils; +import org.springframework.web.util.ContentCachingRequestWrapper; @Component @RequiredArgsConstructor @@ -30,26 +34,32 @@ public class SlackMessageGenerater { public Payload generateErrorMsg(SlackErrorEvent event) throws IOException { final Exception e = event.getException(); + final ContentCachingRequestWrapper cachedRequest = event.getCachedRequest(); List layoutBlocks = new ArrayList<>(); // 제목 layoutBlocks.add(HeaderBlock.builder().text(plainText("에러 알림")).build()); - // 구분선 layoutBlocks.add(new DividerBlock()); - // timeØ - layoutBlocks.add(getTime()); - // IP + Method, Addr + // time, Addr + layoutBlocks.add(makeSection(getTime(), getAddr(cachedRequest))); + // RequestBody + RequestParam + layoutBlocks.add(makeSection(getBody(cachedRequest), getParam(cachedRequest))); + layoutBlocks.add(new DividerBlock()); layoutBlocks.add(makeSection(getErrMessage(e), getErrStack(e))); return Payload.builder().text("에러 알림").blocks(layoutBlocks).build(); } - private LayoutBlock getTime() { + private LayoutBlock getTimeBlock() { MarkdownTextObject timeObj = MarkdownTextObject.builder().text("* Time :*\n" + LocalDateTime.now()).build(); return Blocks.section(section -> section.fields(List.of(timeObj))); } + private MarkdownTextObject getTime() { + return MarkdownTextObject.builder().text("* Time :*\n" + LocalDateTime.now()).build(); + } + private LayoutBlock makeSection(TextObject first, TextObject second) { return Blocks.section(section -> section.fields(List.of(first, second))); } @@ -66,6 +76,25 @@ private MarkdownTextObject getErrStack(Throwable throwable) { return MarkdownTextObject.builder().text("* Stack Trace :*\n" + errorStack).build(); } + private MarkdownTextObject getAddr(ContentCachingRequestWrapper c) { + final String method = c.getMethod(); + final String url = c.getRequestURL().toString(); + return MarkdownTextObject.builder() + .text("* Request Addr :*\n" + method + " : " + url) + .build(); + } + + private MarkdownTextObject getBody(ContentCachingRequestWrapper c) throws IOException { + ServletInputStream inputStream = c.getInputStream(); + String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + return MarkdownTextObject.builder().text("* Request Body :*\n" + messageBody).build(); + } + + private MarkdownTextObject getParam(ContentCachingRequestWrapper c) { + final String param = c.getQueryString(); + return MarkdownTextObject.builder().text("* Request Param :*\n" + param).build(); + } + public Payload generateAsyncErrorMsg(SlackAsyncErrorEvent event) { String name = event.getName(); Throwable throwable = event.getThrowable(); @@ -88,7 +117,7 @@ public Payload generateAsyncErrorMsg(SlackAsyncErrorEvent event) { section.fields(List.of(errorUserIdMarkdown, errorUserIpMarkdown)))); layoutBlocks.add(divider()); - layoutBlocks.add(getTime()); + layoutBlocks.add(getTimeBlock()); String errorStack = getErrorStack(throwable); String message = throwable.toString(); MarkdownTextObject errorNameMarkdown =