perbaikan decompres

This commit is contained in:
2025-09-10 18:08:31 +07:00
parent e61eee5f76
commit 7d03b5d5ef
4 changed files with 386 additions and 47 deletions

View File

@@ -5,8 +5,12 @@ package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"reflect"
"strconv"
"strings"
"sync"
"time"
"api-service/internal/config"
@@ -48,6 +52,224 @@ func NewPesertaHandler(cfg PesertaHandlerConfig) *PesertaHandler {
}
}
// min returns the minimum of two integers
func min(a, b int) int {
if a < b {
return a
}
return b
}
// cleanResponse removes invalid characters and BOM from the response string
func cleanResponse(resp string) string {
// Remove UTF-8 BOM
// Konversi string ke byte slice untuk pengecekan BOM
data := []byte(resp)
// Cek dan hapus semua jenis representasi UTF-8 BOM
// 1. Byte sequence: EF BB BF
if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
data = data[3:]
}
// 2. Unicode character: U+FEFF
if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
data = data[3:]
}
// 3. Zero Width No-Break Space (Unicode)
if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
data = data[3:]
}
// 4. Representasi heksadesimal lainnya
if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
data = data[3:]
}
// Konversi kembali ke string
resp = string(data)
// Hapus karakter null
// Hapus semua karakter kontrol ASCII (0-31) kecuali whitespace yang valid
controlChars := []string{
"\x00", // Null character
"\x01", // Start of Heading
"\x02", // Start of Text
"\x03", // End of Text
"\x04", // End of Transmission (EOT)
"\x05", // Enquiry
"\x06", // Acknowledge
"\x07", // Bell
"\x08", // Backspace
"\x0B", // Vertical Tab
"\x0C", // Form Feed
"\x0E", // Shift Out
"\x0F", // Shift In
"\x10", // Data Link Escape
"\x11", // Device Control 1
"\x12", // Device Control 2
"\x13", // Device Control 3
"\x14", // Device Control 4
"\x15", // Negative Acknowledge
"\x16", // Synchronous Idle
"\x17", // End of Transmission Block
"\x18", // Cancel
"\x19", // End of Medium
"\x1A", // Substitute
"\x1B", // Escape
"\x1C", // File Separator
"\x1D", // Group Separator
"\x1E", // Record Separator
"\x1F", // Unit Separator
}
for _, char := range controlChars {
resp = strings.ReplaceAll(resp, char, "")
}
// Hapus karakter invalid termasuk backtick
invalidChars := []string{
"¢", // Cent sign
"\u00a2", // Cent sign Unicode
"\u0080", // Control character
"`", // Backtick
"´", // Acute accent
"", // Left single quote
"", // Right single quote
"“", // Left double quote
"”", // Right double quote
}
for _, char := range invalidChars {
resp = strings.ReplaceAll(resp, char, "")
}
// Gunakan buffer pool untuk efisiensi memori
var bufPool = sync.Pool{
New: func() interface{} {
return &strings.Builder{}
},
}
buf := bufPool.Get().(*strings.Builder)
defer func() {
buf.Reset()
bufPool.Put(buf)
}()
// Definisikan karakter yang diperbolehkan
allowedChars := map[rune]bool{
'\n': true, '\r': true, '\t': true,
// Tambahkan karakter non-ASCII yang diperbolehkan jika adafalse
// Contoh:
// Latin-1 Supplement
// ASCII printable (32-126) kecuali backtick (96)
'!': true, '"': true, '#': true, '$': true, '%': true, '&': true,
'\'': true, '(': true, ')': true, '*': true, '+': true, ',': true,
'-': true, '.': true, '/': true, '0': true, '1': true, '2': true,
'3': true, '4': true, '5': true, '6': true, '7': true, '8': true,
'9': true, ':': true, ';': true, '<': true, '=': true, '>': true,
'?': true, '@': true, 'A': true, 'B': true, 'C': true, 'D': true,
'E': true, 'F': true, 'G': true, 'H': true, 'I': true, 'J': true,
'K': true, 'L': true, 'M': true, 'N': true, 'O': true, 'P': true,
'Q': true, 'R': true, 'S': true, 'T': true, 'U': true, 'V': true,
'W': true, 'X': true, 'Y': true, 'Z': true, '[': true, '\\': true,
']': true, '^': true, '_': true, 'a': true, 'b': true, 'c': true,
'd': true, 'e': true, 'f': true, 'g': true, 'h': true, 'i': true,
'j': true, 'k': true, 'l': true, 'm': true, 'n': true, 'o': true,
'p': true, 'q': true, 'r': true, 's': true, 't': true, 'u': true,
'v': true, 'w': true, 'x': true, 'y': true, 'z': true, '{': true,
'|': true, '}': true, '~': true,
// Latin-1 Supplement
'¡': true, '¢': true, '£': true, '¤': true, '¥': true, '¦': true,
'§': true, '¨': true, '©': true, 'ª': true, '«': true, '¬': true,
'®': true, '¯': true, '°': true, '±': true, '²': true, '³': true,
'´': true, 'µ': true, '¶': true, '·': true, '¸': true, '¹': true,
'º': true, '»': true, '¼': true, '½': true, '¾': true, '¿': true,
// Huruf Latin dengan diakritik (Lowercase)
'á': true, 'é': true, 'í': true, 'ó': true, 'ú': true, 'ý': true, 'þ': true,
'à': true, 'è': true, 'ì': true, 'ò': true, 'ù': true,
'â': true, 'ê': true, 'î': true, 'ô': true, 'û': true,
'ä': true, 'ë': true, 'ï': true, 'ö': true, 'ü': true, 'ÿ': true,
'ã': true, 'õ': true, 'ñ': true, 'ç': true,
'ā': true, 'ē': true, 'ī': true, 'ō': true, 'ū': true,
'ă': true, 'đ': true, 'ħ': true, 'ij': true, 'ĸ': true, 'ł': true,
'ŋ': true, 'œ': true, 'ŧ': true, 'ß': true,
// Huruf Latin dengan diakritik (Uppercase)
'Á': true, 'É': true, 'Í': true, 'Ó': true, 'Ú': true, 'Ý': true, 'Þ': true,
'À': true, 'È': true, 'Ì': true, 'Ò': true, 'Ù': true,
'Â': true, 'Ê': true, 'Î': true, 'Ô': true, 'Û': true,
'Ä': true, 'Ë': true, 'Ï': true, 'Ö': true, 'Ü': true,
'Ã': true, 'Õ': true, 'Ñ': true, 'Ç': true,
'Ā': true, 'Ē': true, 'Ī': true, 'Ō': true, 'Ū': true,
'Ă': true, 'Đ': true, 'Ħ': true, 'IJ': true, 'Ł': true,
'Ŋ': true, 'Œ': true, 'Ŧ': true, 'ẞ': true,
// Karakter Nordik dan lainnya
'Å': true, 'å': true, 'Æ': true, 'æ': true, 'Ø': true, 'ø': true,
'ſ': true, 'ʼn': true, 'ŀ': true,
// Tanda baca dan simbol matematika
'': true, '': true, '—': true, '―': true, '‖': true, '‗': true,
'†': true, '‡': true, '•': true, '‣': true, '': true, '‥': true,
'…': true, '‧': true, '‰': true, '': true, '″': true, '‴': true,
'': true, '‶': true, '‷': true, '‸': true, '': true, '': true,
'※': true,
// Simbol mata uang (hanya yang umum)
'€': true, '₹': true,
// Karakter lain yang mungkin diperlukan
}
// Filter karakter menggunakan buffer pool
for _, r := range resp {
if r < 128 || allowedChars[r] {
buf.WriteRune(r)
}
}
// Trim whitespace
result := strings.TrimSpace(buf.String())
return result
}
// extractCode extracts the code field from metaData using reflection
func extractCode(metaData interface{}) interface{} {
v := reflect.ValueOf(metaData)
switch v.Kind() {
case reflect.Struct:
codeField := v.FieldByName("Code")
if codeField.IsValid() {
return codeField.Interface()
}
case reflect.Map:
if m, ok := metaData.(map[string]interface{}); ok {
return m["code"]
}
case reflect.String:
var metaMap map[string]interface{}
if err := json.Unmarshal([]byte(metaData.(string)), &metaMap); err == nil {
return metaMap["code"]
}
}
return nil
}
// parseHTTPStatusCode extracts HTTP status code from error message
func parseHTTPStatusCode(errMsg string) int {
if strings.Contains(errMsg, "HTTP error:") {
parts := strings.Split(errMsg, "HTTP error:")
if len(parts) > 1 {
statusPart := strings.TrimSpace(parts[1])
if statusCode, err := strconv.Atoi(strings.Fields(statusPart)[0]); err == nil {
return statusCode
}
}
}
return 500 // Default to internal server error
}
func (h *PesertaHandler) isValidJSON(str string) bool {
var js interface{}
return json.Unmarshal([]byte(str), &js) == nil
}
// GetBynik godoc
// @Summary Get Bynik data
// @Description Get participant eligibility information by NIK
@@ -64,6 +286,9 @@ func NewPesertaHandler(cfg PesertaHandlerConfig) *PesertaHandler {
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
// @Router /Peserta/nik/:nik [get]
func (h *PesertaHandler) GetBynik(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Generate request ID if not present
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
@@ -87,20 +312,21 @@ func (h *PesertaHandler) GetBynik(c *gin.Context) {
}
// Note: dbConn is available for future database operations (e.g., caching, logging)
_ = dbConn // Prevent unused variable warning
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Context Paramaeter
now := time.Now()
dateStr := now.Format("2006-01-02")
fmt.Println("Date (YYYY-MM-DD):", dateStr)
h.logger.Info("Processing GetBynik request", map[string]interface{}{
"request_id": requestID,
"endpoint": "/peserta/nik/:nik",
"endpoint": "/Peserta/nik/:nik/tglSEP/" + dateStr,
"nik": c.Param("nik"),
})
// Extract path parameters
nik := c.Param("nik")
if nik == "" {
if nik == "" || nik == ":nik" {
h.logger.Error("Missing required parameter nik", map[string]interface{}{
"request_id": requestID,
@@ -108,16 +334,14 @@ func (h *PesertaHandler) GetBynik(c *gin.Context) {
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
Status: "error",
Message: "Missing required parameter nik",
Message: "Parameter NIK Masih Kosong / Isi Dahulu NIK!",
RequestID: requestID,
})
return
}
// Call service method
var response peserta.PesertaResponse
endpoint := "/peserta/nik/:nik"
endpoint := "/Peserta/nik/:nik/tglSEP/" + dateStr
endpoint = strings.Replace(endpoint, ":nik", nik, 1)
@@ -168,7 +392,28 @@ func (h *PesertaHandler) GetBynik(c *gin.Context) {
})
} else {
json.Unmarshal([]byte(decryptedResp), response.Data)
// Clean the decrypted response
cleanedResp := cleanResponse(decryptedResp)
if h.isValidJSON(cleanedResp) {
// Unmarshal kembali setelah dibersihkan
err = json.Unmarshal([]byte(cleanedResp), response.Data)
if err != nil {
h.logger.Warn("Failed to unmarshal decrypted response", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
"response_preview": cleanedResp[:min(100, len(cleanedResp))], // Log first 100 chars for debugging
})
// Set Data to nil if unmarshal fails to avoid sending empty struct
response.Data = nil
}
} else {
h.logger.Warn("Invalid JSON in data, storing as string", map[string]interface{}{
"request_id": requestID,
"response": cleanedResp,
})
response.Data.RawResponse = cleanedResp
}
}
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
// Response is already unmarshaled JSON
@@ -186,7 +431,15 @@ func (h *PesertaHandler) GetBynik(c *gin.Context) {
// Ensure response has proper fields
response.Status = "success"
response.RequestID = requestID
c.JSON(http.StatusOK, response)
// Ambil status code dari metaData.code
var statusCode int
code := extractCode(response.MetaData)
if code != nil {
statusCode = models.GetStatusCodeFromMeta(code)
} else {
statusCode = 200
}
c.JSON(statusCode, response)
}
// GetBynokartu godoc
@@ -215,17 +468,37 @@ func (h *PesertaHandler) GetBynokartu(c *gin.Context) {
c.Header("X-Request-ID", requestID)
}
// Get database connection
dbConn, err := h.db.GetDB("postgres_satudata")
if err != nil {
h.logger.Error("Database connection failed", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
Status: "error",
Message: "Database connection failed",
RequestID: requestID,
})
return
}
// Note: dbConn is available for future database operations (e.g., caching, logging)
_ = dbConn // Prevent unused variable warning
// Context Paramaeter
now := time.Now()
dateStr := now.Format("2006-01-02")
fmt.Println("Date (YYYY-MM-DD):", dateStr)
h.logger.Info("Processing GetBynokartu request", map[string]interface{}{
"request_id": requestID,
"endpoint": "/peserta/:nokartu",
"nokartu": c.Param("nokartu"),
"endpoint": "/Peserta/nokartu/:nokartu/tglSEP/" + dateStr,
"nik": c.Param("nokartu"),
})
// Extract path parameters
nokartu := c.Param("nokartu")
if nokartu == "" {
if nokartu == "" || nokartu == ":nokartu" {
h.logger.Error("Missing required parameter nokartu", map[string]interface{}{
"request_id": requestID,
@@ -233,24 +506,36 @@ func (h *PesertaHandler) GetBynokartu(c *gin.Context) {
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
Status: "error",
Message: "Missing required parameter nokartu",
Message: "Parameter Nomor Kartu Bpjs Masih Kosong / Isi Dahulu Nomor Kartu!",
RequestID: requestID,
})
return
}
// Call service method
var response peserta.PesertaResponse
endpoint := "/peserta/:nokartu"
endpoint := "/Peserta/nokartu/:nokartu/tglSEP/" + dateStr
endpoint = strings.Replace(endpoint, ":nokartu", nokartu, 1)
resp, err := h.service.GetRawResponse(ctx, endpoint)
if err != nil {
// Check if error message contains 404 status code
if strings.Contains(err.Error(), "HTTP error: 404") {
h.logger.Error("ByNoKartu not found", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
h.logger.Error("Failed to get Bynokartu", map[string]interface{}{
c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{
Status: "error",
Message: "ByNoKartu not found",
RequestID: requestID,
})
return
}
h.logger.Error("Failed to get ByNoKartu", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
@@ -279,7 +564,18 @@ func (h *PesertaHandler) GetBynokartu(c *gin.Context) {
})
} else {
json.Unmarshal([]byte(decryptedResp), response.Data)
// Clean the decrypted response
cleanedResp := cleanResponse(decryptedResp)
err = json.Unmarshal([]byte(cleanedResp), response.Data)
if err != nil {
h.logger.Warn("Failed to unmarshal decrypted response", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
"response_preview": cleanedResp[:min(100, len(cleanedResp))], // Log first 100 chars for debugging
})
// Set Data to nil if unmarshal fails to avoid sending empty struct
response.Data = nil
}
}
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
// Response is already unmarshaled JSON
@@ -297,30 +593,13 @@ func (h *PesertaHandler) GetBynokartu(c *gin.Context) {
// Ensure response has proper fields
response.Status = "success"
response.RequestID = requestID
c.JSON(http.StatusOK, response)
// Ambil status code dari metaData.code
var statusCode int
code := extractCode(response.MetaData)
if code != nil {
statusCode = models.GetStatusCodeFromMeta(code)
} else {
statusCode = 200
}
// Enhanced error handling
func (h *PesertaHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) {
requestID := c.GetHeader("X-Request-ID")
h.logger.Error(message, map[string]interface{}{
"error": err.Error(),
"status_code": statusCode,
"request_id": requestID,
})
h.respondError(c, message, err, statusCode)
}
func (h *PesertaHandler) respondError(c *gin.Context, message string, err error, statusCode int) {
requestID := c.GetHeader("X-Request-ID")
errorMessage := message
if gin.Mode() == gin.ReleaseMode {
errorMessage = "Internal server error"
}
c.JSON(statusCode, models.ErrorResponseBpjs{
Status: "error",
Message: errorMessage,
RequestID: requestID,
})
c.JSON(statusCode, response)
}

View File

@@ -3,6 +3,8 @@ package models
import (
"database/sql"
"database/sql/driver"
"net/http"
"strconv"
"time"
)
@@ -161,6 +163,42 @@ type MetaInfo struct {
ServerTime string `json:"server_time"`
}
func GetStatusCodeFromMeta(metaCode interface{}) int {
statusCode := http.StatusOK
if metaCode != nil {
switch v := metaCode.(type) {
case string:
if code, err := strconv.Atoi(v); err == nil {
if code >= 100 && code <= 599 {
statusCode = code
} else {
statusCode = http.StatusInternalServerError
}
} else {
statusCode = http.StatusInternalServerError
}
case int:
if v >= 100 && v <= 599 {
statusCode = v
} else {
statusCode = http.StatusInternalServerError
}
case float64:
code := int(v)
if code >= 100 && code <= 599 {
statusCode = code
} else {
statusCode = http.StatusInternalServerError
}
default:
statusCode = http.StatusInternalServerError
}
}
return statusCode
}
// Validation constants
const (
StatusDraft = "draft"

View File

@@ -60,6 +60,7 @@ type PesertaData struct {
NoMR string `json:"noMR"`
NoTelepon string `json:"noTelepon"`
} `json:"mr,omitempty"`
RawResponse string `json:"raw_response,omitempty"`
}
// PesertaResponse represents peserta API response

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"strings"
"time"
"api-service/internal/config"
@@ -16,6 +17,18 @@ import (
"github.com/rs/zerolog/log"
)
// cleanResponse removes invalid characters and BOM from the response string
func cleanResponse(resp string) string {
// Remove UTF-8 BOM
resp = strings.TrimPrefix(resp, "\xef\xbb\xbf")
resp = strings.TrimPrefix(resp, "\ufeff")
// Remove null characters
resp = strings.ReplaceAll(resp, "\x00", "")
// Trim whitespace
resp = strings.TrimSpace(resp)
return resp
}
// VClaimService interface for VClaim operations
type VClaimService interface {
Get(ctx context.Context, endpoint string, result interface{}) error
@@ -172,6 +185,12 @@ func (s *Service) processResponse(res *http.Response) (*ResponDTOVclaim, error)
return finalResp, nil
}
// Check if response looks like HTML or error message (don't try to decrypt)
if strings.HasPrefix(respMentah.Response, "<") || strings.Contains(respMentah.Response, "error") {
finalResp.Response = respMentah.Response
return finalResp, nil
}
// Decrypt response
consID, secretKey, _, tstamp, _ := s.config.SetHeader()
decryptionKey := consID + secretKey + tstamp
@@ -190,6 +209,8 @@ func (s *Service) processResponse(res *http.Response) (*ResponDTOVclaim, error)
// Try to unmarshal decrypted response as JSON
if respDecrypt != "" {
// Clean the decrypted response
respDecrypt = cleanResponse(respDecrypt)
if err := json.Unmarshal([]byte(respDecrypt), &finalResp.Response); err != nil {
// If JSON unmarshal fails, store as string
log.Warn().Err(err).Msg("Failed to unmarshal decrypted response, storing as string")